From eba7471a6c28507657b03ae97ff704491b7a4960 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 17 Mar 2022 14:02:03 +0100 Subject: [PATCH 1/2] Fixed empty string to URLs conversion Closes #919. Signed-off-by: Akos Kitta --- .../dialogs/settings/settings-component.tsx | 85 ++++++++++++++++--- .../dialogs/settings/settings-dialog.tsx | 10 +-- .../src/browser/dialogs/settings/settings.tsx | 21 +++-- .../src/common/protocol/config-service.ts | 31 ++++++- arduino-ide-extension/src/common/types.ts | 4 - .../src/test/common/config-service.test.ts | 35 ++++++++ 6 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 arduino-ide-extension/src/test/common/config-service.test.ts diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx index e7fa7a060..9c1563547 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx @@ -9,6 +9,7 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { + AdditionalUrls, CompilerWarningLiterals, Network, ProxySettings, @@ -35,21 +36,32 @@ export class SettingsComponent extends React.Component< if ( this.state && prevState && - JSON.stringify(this.state) !== JSON.stringify(prevState) + JSON.stringify(SettingsComponent.State.toSettings(this.state)) !== + JSON.stringify(SettingsComponent.State.toSettings(prevState)) ) { - this.props.settingsService.update(this.state, true); + this.props.settingsService.update( + SettingsComponent.State.toSettings(this.state), + true + ); } } componentDidMount(): void { this.props.settingsService .settings() - .then((settings) => this.setState(settings)); - this.toDispose.push( + .then((settings) => + this.setState(SettingsComponent.State.fromSettings(settings)) + ); + this.toDispose.pushAll([ this.props.settingsService.onDidChange((settings) => - this.setState(settings) - ) - ); + this.setState((prevState) => ({ + ...SettingsComponent.State.merge(prevState, settings), + })) + ), + this.props.settingsService.onDidReset((settings) => + this.setState(SettingsComponent.State.fromSettings(settings)) + ), + ]); } componentWillUnmount(): void { @@ -290,8 +302,8 @@ export class SettingsComponent extends React.Component< => { const additionalUrls = await new AdditionalUrlsDialog( - this.state.additionalUrls, + AdditionalUrls.parse(this.state.rawAdditionalUrlsValue, ','), this.props.windowService ).open(); if (additionalUrls) { - this.setState({ additionalUrls }); + this.setState({ + rawAdditionalUrlsValue: AdditionalUrls.stringify(additionalUrls), + }); } }; @@ -492,11 +506,11 @@ export class SettingsComponent extends React.Component< } }; - protected additionalUrlsDidChange = ( + protected rawAdditionalUrlsValueDidChange = ( event: React.ChangeEvent ): void => { this.setState({ - additionalUrls: event.target.value.split(',').map((url) => url.trim()), + rawAdditionalUrlsValue: event.target.value, }); }; @@ -699,5 +713,48 @@ export namespace SettingsComponent { readonly windowService: WindowService; readonly localizationProvider: AsyncLocalizationProvider; } - export type State = Settings & { languages: string[] }; + export type State = Settings & { + rawAdditionalUrlsValue: string; + }; + export namespace State { + export function fromSettings(settings: Settings): State { + return { + ...settings, + rawAdditionalUrlsValue: AdditionalUrls.stringify( + settings.additionalUrls + ), + }; + } + export function toSettings(state: State): Settings { + const parsedAdditionalUrls = AdditionalUrls.parse( + state.rawAdditionalUrlsValue, + ',' + ); + return { + ...state, + additionalUrls: AdditionalUrls.sameAs( + state.additionalUrls, + parsedAdditionalUrls + ) + ? state.additionalUrls + : parsedAdditionalUrls, + }; + } + export function merge(prevState: State, settings: Settings): State { + const prevAdditionalUrls = AdditionalUrls.parse( + prevState.rawAdditionalUrlsValue, + ',' + ); + return { + ...settings, + rawAdditionalUrlsValue: prevState.rawAdditionalUrlsValue, + additionalUrls: AdditionalUrls.sameAs( + prevAdditionalUrls, + settings.additionalUrls + ) + ? prevAdditionalUrls + : settings.additionalUrls, + }; + } + } } diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx index 67bf4d3d6..9e00eaf78 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx @@ -11,6 +11,7 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil import { nls } from '@theia/core/lib/common'; import { SettingsComponent } from './settings-component'; import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization'; +import { AdditionalUrls } from '../../../common/protocol'; @injectable() export class SettingsWidget extends ReactWidget { @@ -96,7 +97,7 @@ export class SettingsDialog extends AbstractDialog> { this.update(); } - protected onUpdateRequest(msg: Message) { + protected onUpdateRequest(msg: Message): void { super.onUpdateRequest(msg); this.widget.update(); } @@ -105,7 +106,7 @@ export class SettingsDialog extends AbstractDialog> { super.onActivateRequest(msg); // calling settingsService.reset() in order to reload the settings from the preferenceService - // and update the UI including changes triggerd from the command palette + // and update the UI including changes triggered from the command palette this.settingsService.reset(); this.widget.activate(); @@ -168,10 +169,7 @@ export class AdditionalUrlsDialog extends AbstractDialog { } get value(): string[] { - return this.textArea.value - .split('\n') - .map((url) => url.trim()) - .filter((url) => !!url); + return AdditionalUrls.parse(this.textArea.value, 'newline'); } protected onAfterAttach(message: Message): void { diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx index 44fa7bd0e..d16f49da2 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings.tsx @@ -8,8 +8,8 @@ import { ThemeService } from '@theia/core/lib/browser/theming'; import { MaybePromise } from '@theia/core/lib/common/types'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser'; -import { Index } from '../../../common/types'; import { + AdditionalUrls, CompilerWarnings, ConfigService, FileSystemExt, @@ -35,7 +35,7 @@ export const UPLOAD_VERBOSE_SETTING = `${UPLOAD_SETTING}.verbose`; export const UPLOAD_VERIFY_SETTING = `${UPLOAD_SETTING}.verify`; export const SHOW_ALL_FILES_SETTING = `${SKETCHBOOK_SETTING}.showAllFiles`; -export interface Settings extends Index { +export interface Settings { editorFontSize: number; // `editor.fontSize` themeId: string; // `workbench.colorTheme` autoSave: 'on' | 'off'; // `editor.autoSave` @@ -53,7 +53,7 @@ export interface Settings extends Index { sketchbookShowAllFiles: boolean; // `arduino.sketchbook.showAllFiles` sketchbookPath: string; // CLI - additionalUrls: string[]; // CLI + additionalUrls: AdditionalUrls; // CLI network: Network; // CLI } export namespace Settings { @@ -84,6 +84,8 @@ export class SettingsService { protected readonly onDidChangeEmitter = new Emitter>(); readonly onDidChange = this.onDidChangeEmitter.event; + protected readonly onDidResetEmitter = new Emitter>(); + readonly onDidReset = this.onDidResetEmitter.event; protected ready = new Deferred(); protected _settings: Settings; @@ -167,7 +169,10 @@ export class SettingsService { async update(settings: Settings, fireDidChange = false): Promise { await this.ready.promise; for (const key of Object.keys(settings)) { - this._settings[key] = settings[key]; + if (key in this._settings) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this._settings as any)[key] = (settings as any)[key]; + } } if (fireDidChange) { this.onDidChangeEmitter.fire(this._settings); @@ -176,7 +181,8 @@ export class SettingsService { async reset(): Promise { const settings = await this.loadSettings(); - return this.update(settings, true); + await this.update(settings, false); + this.onDidResetEmitter.fire(this._settings); } async validate( @@ -267,7 +273,10 @@ export class SettingsService { // after saving all the settings, if we need to change the language we need to perform a reload // Only reload if the language differs from the current locale. `nls.locale === undefined` signals english as well - if (currentLanguage !== nls.locale && !(currentLanguage === 'en' && nls.locale === undefined)) { + if ( + currentLanguage !== nls.locale && + !(currentLanguage === 'en' && nls.locale === undefined) + ) { if (currentLanguage === 'en') { window.localStorage.removeItem(nls.localeId); } else { diff --git a/arduino-ide-extension/src/common/protocol/config-service.ts b/arduino-ide-extension/src/common/protocol/config-service.ts index aec5f20ea..b1c6285a1 100644 --- a/arduino-ide-extension/src/common/protocol/config-service.ts +++ b/arduino-ide-extension/src/common/protocol/config-service.ts @@ -116,7 +116,7 @@ export interface Config { readonly sketchDirUri: string; readonly dataDirUri: string; readonly downloadsDirUri: string; - readonly additionalUrls: string[]; + readonly additionalUrls: AdditionalUrls; readonly network: Network; readonly daemon: Daemon; } @@ -141,3 +141,32 @@ export namespace Config { ); } } +export type AdditionalUrls = string[]; +export namespace AdditionalUrls { + export function parse(value: string, delimiter: ',' | 'newline'): string[] { + return value + .trim() + .split(delimiter === ',' ? delimiter : /\r?\n/) + .map((url) => url.trim()) + .filter((url) => !!url); + } + export function stringify(additionalUrls: AdditionalUrls): string { + return additionalUrls.join(','); + } + export function sameAs(left: AdditionalUrls, right: AdditionalUrls): boolean { + if (left.length !== right.length) { + return false; + } + const localeCompare = (left: string, right: string) => + left.localeCompare(right); + const normalize = (url: string) => url.toLowerCase(); + const normalizedLeft = left.map(normalize).sort(localeCompare); + const normalizedRight = right.map(normalize).sort(localeCompare); + for (let i = 0; i < normalizedLeft.length; i++) { + if (normalizedLeft[i] !== normalizedRight[i]) { + return false; + } + } + return true; + } +} diff --git a/arduino-ide-extension/src/common/types.ts b/arduino-ide-extension/src/common/types.ts index 85f85f643..421a27534 100644 --- a/arduino-ide-extension/src/common/types.ts +++ b/arduino-ide-extension/src/common/types.ts @@ -1,7 +1,3 @@ export type RecursiveRequired = { [P in keyof T]-?: RecursiveRequired; }; - -export interface Index { - [key: string]: any; -} diff --git a/arduino-ide-extension/src/test/common/config-service.test.ts b/arduino-ide-extension/src/test/common/config-service.test.ts new file mode 100644 index 000000000..aae179770 --- /dev/null +++ b/arduino-ide-extension/src/test/common/config-service.test.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai'; +import { AdditionalUrls } from '../../common/protocol'; + +describe('config-service', () => { + describe('additionalUrls', () => { + it('should consider additional URLs same as if they differ in case', () => { + expect(AdditionalUrls.sameAs(['aaaa'], ['AAAA'])).to.be.true; + }); + it('should consider additional URLs same as if they have a different order', () => { + expect(AdditionalUrls.sameAs(['bbbb', 'aaaa'], ['aaaa', 'bbbb'])).to.be + .true; + }); + it('should parse an empty string as an empty array', () => { + expect(AdditionalUrls.parse('', ',')).to.be.empty; + }); + it('should parse a blank string as an empty array', () => { + expect(AdditionalUrls.parse(' ', ',')).to.be.empty; + }); + it('should parse urls with commas', () => { + expect(AdditionalUrls.parse(' ,a , b , c, ', ',')).to.be.deep.equal([ + 'a', + 'b', + 'c', + ]); + }); + it("should parse urls with both '\\n' and '\\r\\n' line endings", () => { + expect( + AdditionalUrls.parse( + 'a ' + '\r\n' + ' b ' + '\n' + ' c ' + '\r\n' + ' ' + '\n' + '', + 'newline' + ) + ).to.be.deep.equal(['a', 'b', 'c']); + }); + }); +}); From 51aef8a02063ad70ba405b610d87f80429eafba9 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 17 Mar 2022 14:17:06 +0100 Subject: [PATCH 2/2] #881: Fixed height of the 3rd part URLs `textarea` Closes #881. Signed-off-by: Akos Kitta --- arduino-ide-extension/src/browser/style/settings-dialog.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arduino-ide-extension/src/browser/style/settings-dialog.css b/arduino-ide-extension/src/browser/style/settings-dialog.css index 15b5f5bd6..44a641943 100644 --- a/arduino-ide-extension/src/browser/style/settings-dialog.css +++ b/arduino-ide-extension/src/browser/style/settings-dialog.css @@ -57,3 +57,7 @@ display: flex; justify-content: center; } + +.p-Widget.dialogOverlay .dialogBlock .dialogContent.additional-urls-dialog { + display: block; +}