Skip to content

Commit 6c28147

Browse files
committed
Fixed empty string to URLs conversion
Closes arduino#919. Signed-off-by: Akos Kitta <[email protected]>
1 parent f36df02 commit 6c28147

File tree

6 files changed

+149
-32
lines changed

6 files changed

+149
-32
lines changed

Diff for: arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx

+67-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service';
99
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service';
1010
import { DisposableCollection } from '@theia/core/lib/common/disposable';
1111
import {
12+
AdditionalUrls,
1213
CompilerWarningLiterals,
1314
Network,
1415
ProxySettings,
@@ -35,21 +36,32 @@ export class SettingsComponent extends React.Component<
3536
if (
3637
this.state &&
3738
prevState &&
38-
JSON.stringify(this.state) !== JSON.stringify(prevState)
39+
JSON.stringify(SettingsComponent.State.toSettings(this.state)) !==
40+
JSON.stringify(SettingsComponent.State.toSettings(prevState))
3941
) {
40-
this.props.settingsService.update(this.state, true);
42+
this.props.settingsService.update(
43+
SettingsComponent.State.toSettings(this.state),
44+
true
45+
);
4146
}
4247
}
4348

4449
componentDidMount(): void {
4550
this.props.settingsService
4651
.settings()
47-
.then((settings) => this.setState(settings));
48-
this.toDispose.push(
52+
.then((settings) =>
53+
this.setState(SettingsComponent.State.fromSettings(settings))
54+
);
55+
this.toDispose.pushAll([
4956
this.props.settingsService.onDidChange((settings) =>
50-
this.setState(settings)
51-
)
52-
);
57+
this.setState((prevState) => ({
58+
...SettingsComponent.State.merge(prevState, settings),
59+
}))
60+
),
61+
this.props.settingsService.onDidReset((settings) =>
62+
this.setState(SettingsComponent.State.fromSettings(settings))
63+
),
64+
]);
5365
}
5466

5567
componentWillUnmount(): void {
@@ -290,8 +302,8 @@ export class SettingsComponent extends React.Component<
290302
<input
291303
className="theia-input stretch with-margin"
292304
type="text"
293-
value={this.state.additionalUrls.join(',')}
294-
onChange={this.additionalUrlsDidChange}
305+
value={this.state.rawAdditionalUrlsValue}
306+
onChange={this.rawAdditionalUrlsValueDidChange}
295307
/>
296308
<i
297309
className="fa fa-window-restore theia-button shrink"
@@ -475,11 +487,11 @@ export class SettingsComponent extends React.Component<
475487

476488
protected editAdditionalUrlDidClick = async (): Promise<void> => {
477489
const additionalUrls = await new AdditionalUrlsDialog(
478-
this.state.additionalUrls,
490+
AdditionalUrls.parse(this.state.rawAdditionalUrlsValue, ','),
479491
this.props.windowService
480492
).open();
481493
if (additionalUrls) {
482-
this.setState({ additionalUrls });
494+
this.setState({ rawAdditionalUrlsValue: additionalUrls.join(',') });
483495
}
484496
};
485497

@@ -492,11 +504,11 @@ export class SettingsComponent extends React.Component<
492504
}
493505
};
494506

495-
protected additionalUrlsDidChange = (
507+
protected rawAdditionalUrlsValueDidChange = (
496508
event: React.ChangeEvent<HTMLInputElement>
497509
): void => {
498510
this.setState({
499-
additionalUrls: event.target.value.split(',').map((url) => url.trim()),
511+
rawAdditionalUrlsValue: event.target.value,
500512
});
501513
};
502514

@@ -699,5 +711,46 @@ export namespace SettingsComponent {
699711
readonly windowService: WindowService;
700712
readonly localizationProvider: AsyncLocalizationProvider;
701713
}
702-
export type State = Settings & { languages: string[] };
714+
export type State = Settings & {
715+
rawAdditionalUrlsValue: string;
716+
};
717+
export namespace State {
718+
export function fromSettings(settings: Settings): State {
719+
return {
720+
...settings,
721+
rawAdditionalUrlsValue: settings.additionalUrls.join(','),
722+
};
723+
}
724+
export function toSettings(state: State): Settings {
725+
const parsedAdditionalUrls = AdditionalUrls.parse(
726+
state.rawAdditionalUrlsValue,
727+
','
728+
);
729+
return {
730+
...state,
731+
additionalUrls: AdditionalUrls.sameAs(
732+
state.additionalUrls,
733+
parsedAdditionalUrls
734+
)
735+
? state.additionalUrls
736+
: parsedAdditionalUrls,
737+
};
738+
}
739+
export function merge(prevState: State, settings: Settings): State {
740+
const prevAdditionalUrls = AdditionalUrls.parse(
741+
prevState.rawAdditionalUrlsValue,
742+
','
743+
);
744+
return {
745+
...settings,
746+
rawAdditionalUrlsValue: prevState.rawAdditionalUrlsValue,
747+
additionalUrls: AdditionalUrls.sameAs(
748+
prevAdditionalUrls,
749+
settings.additionalUrls
750+
)
751+
? prevAdditionalUrls
752+
: settings.additionalUrls,
753+
};
754+
}
755+
}
703756
}

Diff for: arduino-ide-extension/src/browser/dialogs/settings/settings-dialog.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil
1111
import { nls } from '@theia/core/lib/common';
1212
import { SettingsComponent } from './settings-component';
1313
import { AsyncLocalizationProvider } from '@theia/core/lib/common/i18n/localization';
14+
import { AdditionalUrls } from '../../../common/protocol';
1415

1516
@injectable()
1617
export class SettingsWidget extends ReactWidget {
@@ -96,7 +97,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
9697
this.update();
9798
}
9899

99-
protected onUpdateRequest(msg: Message) {
100+
protected onUpdateRequest(msg: Message): void {
100101
super.onUpdateRequest(msg);
101102
this.widget.update();
102103
}
@@ -105,7 +106,7 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
105106
super.onActivateRequest(msg);
106107

107108
// calling settingsService.reset() in order to reload the settings from the preferenceService
108-
// and update the UI including changes triggerd from the command palette
109+
// and update the UI including changes triggered from the command palette
109110
this.settingsService.reset();
110111

111112
this.widget.activate();
@@ -168,10 +169,7 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
168169
}
169170

170171
get value(): string[] {
171-
return this.textArea.value
172-
.split('\n')
173-
.map((url) => url.trim())
174-
.filter((url) => !!url);
172+
return AdditionalUrls.parse(this.textArea.value, 'newline');
175173
}
176174

177175
protected onAfterAttach(message: Message): void {

Diff for: arduino-ide-extension/src/browser/dialogs/settings/settings.tsx

+16-7
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { ThemeService } from '@theia/core/lib/browser/theming';
88
import { MaybePromise } from '@theia/core/lib/common/types';
99
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
1010
import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser';
11-
import { Index } from '../../../common/types';
1211
import {
12+
AdditionalUrls,
1313
CompilerWarnings,
1414
ConfigService,
1515
FileSystemExt,
@@ -35,7 +35,7 @@ export const UPLOAD_VERBOSE_SETTING = `${UPLOAD_SETTING}.verbose`;
3535
export const UPLOAD_VERIFY_SETTING = `${UPLOAD_SETTING}.verify`;
3636
export const SHOW_ALL_FILES_SETTING = `${SKETCHBOOK_SETTING}.showAllFiles`;
3737

38-
export interface Settings extends Index {
38+
export interface Settings {
3939
editorFontSize: number; // `editor.fontSize`
4040
themeId: string; // `workbench.colorTheme`
4141
autoSave: 'on' | 'off'; // `editor.autoSave`
@@ -53,7 +53,7 @@ export interface Settings extends Index {
5353
sketchbookShowAllFiles: boolean; // `arduino.sketchbook.showAllFiles`
5454

5555
sketchbookPath: string; // CLI
56-
additionalUrls: string[]; // CLI
56+
additionalUrls: AdditionalUrls; // CLI
5757
network: Network; // CLI
5858
}
5959
export namespace Settings {
@@ -84,6 +84,8 @@ export class SettingsService {
8484

8585
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
8686
readonly onDidChange = this.onDidChangeEmitter.event;
87+
protected readonly onDidResetEmitter = new Emitter<Readonly<Settings>>();
88+
readonly onDidReset = this.onDidResetEmitter.event;
8789

8890
protected ready = new Deferred<void>();
8991
protected _settings: Settings;
@@ -166,8 +168,11 @@ export class SettingsService {
166168

167169
async update(settings: Settings, fireDidChange = false): Promise<void> {
168170
await this.ready.promise;
169-
for (const key of Object.keys(settings)) {
170-
this._settings[key] = settings[key];
171+
for (const prop of Object.getOwnPropertyNames(settings)) {
172+
if (prop in this._settings) {
173+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
174+
(this._settings as any)[prop] = (settings as any)[prop];
175+
}
171176
}
172177
if (fireDidChange) {
173178
this.onDidChangeEmitter.fire(this._settings);
@@ -176,7 +181,8 @@ export class SettingsService {
176181

177182
async reset(): Promise<void> {
178183
const settings = await this.loadSettings();
179-
return this.update(settings, true);
184+
await this.update(settings, false);
185+
this.onDidResetEmitter.fire(this._settings);
180186
}
181187

182188
async validate(
@@ -267,7 +273,10 @@ export class SettingsService {
267273

268274
// after saving all the settings, if we need to change the language we need to perform a reload
269275
// Only reload if the language differs from the current locale. `nls.locale === undefined` signals english as well
270-
if (currentLanguage !== nls.locale && !(currentLanguage === 'en' && nls.locale === undefined)) {
276+
if (
277+
currentLanguage !== nls.locale &&
278+
!(currentLanguage === 'en' && nls.locale === undefined)
279+
) {
271280
if (currentLanguage === 'en') {
272281
window.localStorage.removeItem(nls.localeId);
273282
} else {

Diff for: arduino-ide-extension/src/common/protocol/config-service.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export interface Config {
116116
readonly sketchDirUri: string;
117117
readonly dataDirUri: string;
118118
readonly downloadsDirUri: string;
119-
readonly additionalUrls: string[];
119+
readonly additionalUrls: AdditionalUrls;
120120
readonly network: Network;
121121
readonly daemon: Daemon;
122122
}
@@ -141,3 +141,29 @@ export namespace Config {
141141
);
142142
}
143143
}
144+
export type AdditionalUrls = string[];
145+
export namespace AdditionalUrls {
146+
export function parse(value: string, delimiter: ',' | 'newline'): string[] {
147+
return value
148+
.trim()
149+
.split(delimiter === ',' ? delimiter : /\r?\n/)
150+
.map((url) => url.trim())
151+
.filter((url) => !!url);
152+
}
153+
export function sameAs(left: AdditionalUrls, right: AdditionalUrls): boolean {
154+
if (left.length !== right.length) {
155+
return false;
156+
}
157+
const localeCompare = (left: string, right: string) =>
158+
left.localeCompare(right);
159+
const normalize = (url: string) => url.toLowerCase();
160+
const normalizedLeft = left.map(normalize).sort(localeCompare);
161+
const normalizedRight = right.map(normalize).sort(localeCompare);
162+
for (let i = 0; i < normalizedLeft.length; i++) {
163+
if (normalizedLeft[i] !== normalizedRight[i]) {
164+
return false;
165+
}
166+
}
167+
return true;
168+
}
169+
}

Diff for: arduino-ide-extension/src/common/types.ts

-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
export type RecursiveRequired<T> = {
22
[P in keyof T]-?: RecursiveRequired<T[P]>;
33
};
4-
5-
export interface Index {
6-
[key: string]: any;
7-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect } from 'chai';
2+
import { AdditionalUrls } from '../../common/protocol';
3+
4+
describe('config-service', () => {
5+
describe('additionalUrls', () => {
6+
it('should consider additional URLs same as if they differ in case', () => {
7+
expect(AdditionalUrls.sameAs(['aaaa'], ['AAAA'])).to.be.true;
8+
});
9+
it('should consider additional URLs same as if they have a different order', () => {
10+
expect(AdditionalUrls.sameAs(['bbbb', 'aaaa'], ['aaaa', 'bbbb'])).to.be
11+
.true;
12+
});
13+
it('should parse an empty string as an empty array', () => {
14+
expect(AdditionalUrls.parse('', ',')).to.be.empty;
15+
});
16+
it('should parse a blank string as an empty array', () => {
17+
expect(AdditionalUrls.parse(' ', ',')).to.be.empty;
18+
});
19+
it('should parse urls with commas', () => {
20+
expect(AdditionalUrls.parse(' ,a , b , c, ', ',')).to.be.deep.equal([
21+
'a',
22+
'b',
23+
'c',
24+
]);
25+
});
26+
it("should parse urls with both '\\n' and '\\r\\n' line endings", () => {
27+
expect(
28+
AdditionalUrls.parse(
29+
'a ' + '\r\n' + ' b ' + '\n' + ' c ' + '\r\n' + ' ' + '\n' + '',
30+
'newline'
31+
)
32+
).to.be.deep.equal(['a', 'b', 'c']);
33+
});
34+
});
35+
});

0 commit comments

Comments
 (0)