@theia/preferences 1.76.0-next.6 → 1.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/lib/browser/preference-frontend-contribution.d.ts +18 -0
  2. package/lib/browser/preference-frontend-contribution.d.ts.map +1 -1
  3. package/lib/browser/preference-frontend-contribution.js +48 -12
  4. package/lib/browser/preference-frontend-contribution.js.map +1 -1
  5. package/lib/browser/preference-frontend-contribution.spec.d.ts +2 -0
  6. package/lib/browser/preference-frontend-contribution.spec.d.ts.map +1 -0
  7. package/lib/browser/preference-frontend-contribution.spec.js +76 -0
  8. package/lib/browser/preference-frontend-contribution.spec.js.map +1 -0
  9. package/lib/browser/preference-frontend-module.d.ts.map +1 -1
  10. package/lib/browser/preference-frontend-module.js +4 -0
  11. package/lib/browser/preference-frontend-module.js.map +1 -1
  12. package/lib/browser/preference-tree-model.d.ts +38 -1
  13. package/lib/browser/preference-tree-model.d.ts.map +1 -1
  14. package/lib/browser/preference-tree-model.js +136 -3
  15. package/lib/browser/preference-tree-model.js.map +1 -1
  16. package/lib/browser/preference-tree-model.spec.d.ts +2 -0
  17. package/lib/browser/preference-tree-model.spec.d.ts.map +1 -0
  18. package/lib/browser/preference-tree-model.spec.js +320 -0
  19. package/lib/browser/preference-tree-model.spec.js.map +1 -0
  20. package/lib/browser/session-preference-remote-cli-contribution.d.ts +17 -0
  21. package/lib/browser/session-preference-remote-cli-contribution.d.ts.map +1 -0
  22. package/lib/browser/session-preference-remote-cli-contribution.js +54 -0
  23. package/lib/browser/session-preference-remote-cli-contribution.js.map +1 -0
  24. package/lib/browser/session-preference-remote-cli-contribution.spec.d.ts +2 -0
  25. package/lib/browser/session-preference-remote-cli-contribution.spec.d.ts.map +1 -0
  26. package/lib/browser/session-preference-remote-cli-contribution.spec.js +50 -0
  27. package/lib/browser/session-preference-remote-cli-contribution.spec.js.map +1 -0
  28. package/lib/browser/views/preference-editor-widget.d.ts +29 -7
  29. package/lib/browser/views/preference-editor-widget.d.ts.map +1 -1
  30. package/lib/browser/views/preference-editor-widget.js +119 -15
  31. package/lib/browser/views/preference-editor-widget.js.map +1 -1
  32. package/lib/browser/views/preference-widget.d.ts +1 -2
  33. package/lib/browser/views/preference-widget.d.ts.map +1 -1
  34. package/lib/browser/views/preference-widget.js +0 -2
  35. package/lib/browser/views/preference-widget.js.map +1 -1
  36. package/lib/common/cli-preferences.d.ts +28 -0
  37. package/lib/common/cli-preferences.d.ts.map +1 -1
  38. package/lib/common/cli-preferences.js +66 -1
  39. package/lib/common/cli-preferences.js.map +1 -1
  40. package/lib/common/cli-preferences.spec.d.ts +2 -0
  41. package/lib/common/cli-preferences.spec.d.ts.map +1 -0
  42. package/lib/common/cli-preferences.spec.js +64 -0
  43. package/lib/common/cli-preferences.spec.js.map +1 -0
  44. package/lib/node/preference-cli-contribution.d.ts.map +1 -1
  45. package/lib/node/preference-cli-contribution.js +3 -22
  46. package/lib/node/preference-cli-contribution.js.map +1 -1
  47. package/package.json +9 -9
  48. package/src/browser/preference-frontend-contribution.spec.ts +92 -0
  49. package/src/browser/preference-frontend-contribution.ts +50 -13
  50. package/src/browser/preference-frontend-module.ts +4 -0
  51. package/src/browser/preference-tree-model.spec.ts +362 -0
  52. package/src/browser/preference-tree-model.ts +144 -3
  53. package/src/browser/session-preference-remote-cli-contribution.spec.ts +59 -0
  54. package/src/browser/session-preference-remote-cli-contribution.ts +49 -0
  55. package/src/browser/style/index.css +6 -0
  56. package/src/browser/views/preference-editor-widget.ts +124 -23
  57. package/src/browser/views/preference-widget.tsx +1 -4
  58. package/src/common/cli-preferences.spec.ts +73 -0
  59. package/src/common/cli-preferences.ts +75 -0
  60. package/src/node/preference-cli-contribution.ts +3 -22
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@theia/preferences",
3
- "version": "1.76.0-next.6+5358262f7",
3
+ "version": "1.76.0",
4
4
  "description": "Theia - Preferences Extension",
5
5
  "dependencies": {
6
- "@theia/core": "1.76.0-next.6+5358262f7",
7
- "@theia/editor": "1.76.0-next.6+5358262f7",
8
- "@theia/filesystem": "1.76.0-next.6+5358262f7",
9
- "@theia/monaco": "1.76.0-next.6+5358262f7",
6
+ "@theia/core": "1.76.0",
7
+ "@theia/editor": "1.76.0",
8
+ "@theia/filesystem": "1.76.0",
9
+ "@theia/monaco": "1.76.0",
10
10
  "@theia/monaco-editor-core": "1.108.201",
11
- "@theia/userstorage": "1.76.0-next.6+5358262f7",
12
- "@theia/workspace": "1.76.0-next.6+5358262f7",
11
+ "@theia/userstorage": "1.76.0",
12
+ "@theia/workspace": "1.76.0",
13
13
  "async-mutex": "^0.3.2",
14
14
  "fast-deep-equal": "^3.1.3",
15
15
  "jsonc-parser": "^3.3.1",
@@ -50,10 +50,10 @@
50
50
  "watch": "theiaext watch"
51
51
  },
52
52
  "devDependencies": {
53
- "@theia/ext-scripts": "1.75.0"
53
+ "@theia/ext-scripts": "1.76.0"
54
54
  },
55
55
  "nyc": {
56
56
  "extends": "../../configs/nyc.json"
57
57
  },
58
- "gitHead": "5358262f7ca40ba9a5e7b514914a18202bd2b7e6"
58
+ "gitHead": "b9b54c500b46515512e2b44371bc0c7122947bed"
59
59
  }
@@ -0,0 +1,92 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
19
+
20
+ const launchArgs = (values: Record<string, string[]>): LaunchArguments => ({ values, negated: [] });
21
+ import { PreferenceFrontendContribution } from './preference-frontend-contribution';
22
+
23
+ class TestPreferenceFrontendContribution extends PreferenceFrontendContribution {
24
+
25
+ forwardedArgs: LaunchArguments | undefined;
26
+ sessionBackendCalls = 0;
27
+ persistentBackendCalls = 0;
28
+
29
+ resolve(): Promise<{ session: [string, unknown][], persistent: [string, unknown][] }> {
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ (this as any).launchArgs = { getLaunchArgs: () => this.forwardedArgs };
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ (this as any).CliPreferences = {
34
+ getSessionPreferences: async () => {
35
+ this.sessionBackendCalls++;
36
+ return [['backend.session', 1]] as [string, unknown][];
37
+ },
38
+ getPreferences: async () => {
39
+ this.persistentBackendCalls++;
40
+ return [['backend.persistent', 2]] as [string, unknown][];
41
+ }
42
+ };
43
+ return this.resolveCliPreferences();
44
+ }
45
+ }
46
+
47
+ describe('PreferenceFrontendContribution#resolveCliPreferences', () => {
48
+
49
+ let contribution: TestPreferenceFrontendContribution;
50
+
51
+ beforeEach(() => {
52
+ contribution = new TestPreferenceFrontendContribution();
53
+ });
54
+
55
+ it('reads preferences from the backend on a cold-start window (no forwarded options)', async () => {
56
+ contribution.forwardedArgs = undefined;
57
+
58
+ const result = await contribution.resolve();
59
+
60
+ expect(result.session).to.deep.equal([['backend.session', 1]]);
61
+ expect(result.persistent).to.deep.equal([['backend.persistent', 2]]);
62
+ expect(contribution.sessionBackendCalls).to.equal(1);
63
+ });
64
+
65
+ it('layers --session-preference from the forwarded options on top of the backend values', async () => {
66
+ contribution.forwardedArgs = launchArgs({ 'session-preference': ['editor.fontSize=20'], 'attach-container': ['B'] });
67
+
68
+ const result = await contribution.resolve();
69
+
70
+ // Process-wide backend values are kept (base) and the forwarded value is added on top.
71
+ expect(result.session).to.deep.equal([['backend.session', 1], ['editor.fontSize', 20]]);
72
+ expect(result.persistent).to.deep.equal([['backend.persistent', 2]]);
73
+ expect(contribution.sessionBackendCalls).to.equal(1);
74
+ });
75
+
76
+ it('layers --set-preference from the forwarded options on top of the backend values', async () => {
77
+ contribution.forwardedArgs = launchArgs({ 'set-preference': ['editor.tabSize=2'] });
78
+
79
+ const result = await contribution.resolve();
80
+
81
+ expect(result.persistent).to.deep.equal([['backend.persistent', 2], ['editor.tabSize', 2]]);
82
+ expect(result.session).to.deep.equal([['backend.session', 1]]);
83
+ });
84
+
85
+ it('lets a forwarded value override a same-key backend value', async () => {
86
+ contribution.forwardedArgs = launchArgs({ 'session-preference': ['backend.session=99'] });
87
+
88
+ const result = await contribution.resolve();
89
+
90
+ expect(result.session).to.deep.equal([['backend.session', 99]]);
91
+ });
92
+ });
@@ -16,7 +16,9 @@
16
16
 
17
17
  import { inject, injectable, named } from '@theia/core/shared/inversify';
18
18
  import { FrontendApplicationContribution } from '@theia/core/lib/browser';
19
- import { CliPreferences } from '../common/cli-preferences';
19
+ import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
20
+ import { WindowLaunchArgs } from '@theia/core/lib/browser/window/window-launch-args';
21
+ import { CliPreferences, CliPreferenceEntry } from '../common/cli-preferences';
20
22
  import { PreferenceService, PreferenceScope } from '@theia/core/lib/common/preferences';
21
23
  import { ILogger } from '@theia/core';
22
24
 
@@ -31,23 +33,15 @@ export class PreferenceFrontendContribution implements FrontendApplicationContri
31
33
  @inject(ILogger) @named('preferences:PreferenceFrontendContribution')
32
34
  protected readonly logger: ILogger;
33
35
 
36
+ @inject(WindowLaunchArgs)
37
+ protected readonly launchArgs: WindowLaunchArgs;
38
+
34
39
  onStart(): void {
35
40
  this.applyCliPreferences();
36
41
  }
37
42
 
38
43
  protected async applyCliPreferences(): Promise<void> {
39
- // Fetch both buckets in parallel; both are RPC hops to the same backend and
40
- // can overlap with the preference service initialising its providers.
41
- const [session, persistent] = await Promise.all([
42
- this.CliPreferences.getSessionPreferences().catch(e => {
43
- this.logger.warn('Failed to fetch --session-preference values:', e);
44
- return [] as [string, unknown][];
45
- }),
46
- this.CliPreferences.getPreferences().catch(e => {
47
- this.logger.warn('Failed to fetch --set-preference values:', e);
48
- return [] as [string, unknown][];
49
- })
50
- ]);
44
+ const { session, persistent } = await this.resolveCliPreferences();
51
45
 
52
46
  // `preferenceService.set()` needs the target provider registered in the providers
53
47
  // map, which only happens once `initializeProviders()` has walked every scope and
@@ -70,6 +64,49 @@ export class PreferenceFrontendContribution implements FrontendApplicationContri
70
64
  await this.applyAll(persistent, PreferenceScope.User);
71
65
  }
72
66
 
67
+ /**
68
+ * Resolves the CLI-provided preferences to apply to this window.
69
+ *
70
+ * The shared backend reflects the original cold-start launch (it cannot distinguish between
71
+ * windows), so it supplies the process-wide CLI preferences. A forwarded (second-instance)
72
+ * window additionally carries its own options, provided by the trusted main process, and layers
73
+ * them on top, overriding by key. Merging (rather than replacing) means a plain
74
+ * second-instance window keeps the process-wide values instead of dropping them, while an attach
75
+ * window still gets its own overrides.
76
+ */
77
+ protected async resolveCliPreferences(): Promise<{ session: [string, unknown][], persistent: [string, unknown][] }> {
78
+ // Fetch both buckets in parallel; both are RPC hops to the same backend and
79
+ // can overlap with the preference service initialising its providers.
80
+ const [session, persistent] = await Promise.all([
81
+ this.CliPreferences.getSessionPreferences().catch(e => {
82
+ this.logger.warn('Failed to fetch --session-preference values:', e);
83
+ return [] as [string, unknown][];
84
+ }),
85
+ this.CliPreferences.getPreferences().catch(e => {
86
+ this.logger.warn('Failed to fetch --set-preference values:', e);
87
+ return [] as [string, unknown][];
88
+ })
89
+ ]);
90
+ const forwarded = this.launchArgs.getLaunchArgs();
91
+ if (forwarded === undefined) {
92
+ return { session, persistent };
93
+ }
94
+ const warn = (message: string) => this.logger.warn(message);
95
+ return {
96
+ session: this.mergeEntries(session, CliPreferenceEntry.parseAll(LaunchArguments.values(forwarded, 'session-preference'), warn)),
97
+ persistent: this.mergeEntries(persistent, CliPreferenceEntry.parseAll(LaunchArguments.values(forwarded, 'set-preference'), warn))
98
+ };
99
+ }
100
+
101
+ /** Overlays `overrides` onto `base`, later entries winning per key while preserving order (base first). */
102
+ protected mergeEntries(base: ReadonlyArray<[string, unknown]>, overrides: ReadonlyArray<[string, unknown]>): [string, unknown][] {
103
+ const merged = new Map<string, unknown>();
104
+ for (const [key, value] of [...base, ...overrides]) {
105
+ merged.set(key, value);
106
+ }
107
+ return [...merged];
108
+ }
109
+
73
110
  /**
74
111
  * Applies a batch of CLI-provided preferences sequentially. A rejection from an
75
112
  * individual write (bad key, invalid scope, etc.) is logged and does not abort the
@@ -32,6 +32,8 @@ import { PreferenceOpenHandler } from './preference-open-handler';
32
32
  import { CliPreferences, CliPreferencesPath } from '../common/cli-preferences';
33
33
  import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
34
34
  import { PreferenceFrontendContribution } from './preference-frontend-contribution';
35
+ import { RemoteCliArgsContribution } from '@theia/core/lib/common/remote-cli-args-contribution';
36
+ import { SessionPreferenceRemoteCliContribution } from './session-preference-remote-cli-contribution';
35
37
  import { SessionPreferenceStatusBarContribution } from './session-preference-status-bar-contribution';
36
38
  import { PreferenceLayoutProvider } from './util/preference-layout';
37
39
  import { PreferencesWidget } from './views/preference-widget';
@@ -71,6 +73,8 @@ export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind
71
73
  bind(CliPreferences).toDynamicValue(ctx => ServiceConnectionProvider.createProxy<CliPreferences>(ctx.container, CliPreferencesPath)).inSingletonScope();
72
74
  bind(PreferenceFrontendContribution).toSelf().inSingletonScope();
73
75
  bind(FrontendApplicationContribution).toService(PreferenceFrontendContribution);
76
+ bind(SessionPreferenceRemoteCliContribution).toSelf().inSingletonScope();
77
+ bind(RemoteCliArgsContribution).toService(SessionPreferenceRemoteCliContribution);
74
78
  bind(SessionPreferenceStatusBarContribution).toSelf().inSingletonScope();
75
79
  bind(FrontendApplicationContribution).toService(SessionPreferenceStatusBarContribution);
76
80
 
@@ -0,0 +1,362 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Safi Seid-Ahmad, K2view and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ /* eslint-disable @typescript-eslint/no-explicit-any */
18
+
19
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
20
+ const disableJSDOM = enableJSDOM();
21
+
22
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
23
+ FrontendApplicationConfigProvider.set({});
24
+
25
+ import { expect } from 'chai';
26
+ import { Container } from '@theia/core/shared/inversify';
27
+ import { Emitter, ILogger, PreferenceSchemaService, PreferenceScope, PreferenceService } from '@theia/core';
28
+ import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
29
+ import { CompositeTreeNode, createTreeContainer, LabelProvider } from '@theia/core/lib/browser';
30
+ import { PreferenceTreeModel } from './preference-tree-model';
31
+ import { PreferencesSearchbarWidget } from './views/preference-searchbar-widget';
32
+ import { PreferenceTreeGenerator } from './util/preference-tree-generator';
33
+ import { PreferencesScopeTabBar } from './views/preference-scope-tabbar-widget';
34
+ import { Preference } from './util/preference-types';
35
+ import { COMMONLY_USED_SECTION_PREFIX } from './util/preference-layout';
36
+
37
+ disableJSDOM();
38
+
39
+ interface TreeFixture {
40
+ root: CompositeTreeNode;
41
+ commonlyUsed?: Preference.CompositeTreeNode;
42
+ editor: Preference.CompositeTreeNode;
43
+ editorCursor: Preference.CompositeTreeNode;
44
+ extensions: Preference.CompositeTreeNode;
45
+ }
46
+
47
+ describe('PreferenceTreeModel', () => {
48
+
49
+ let searchbarEmitter: Emitter<string>;
50
+ let schemaEmitter: Emitter<CompositeTreeNode>;
51
+ let scopeEmitter: Emitter<Preference.SelectedScopeDetails>;
52
+ let isValidInScope: (preferenceId: string, scope: number) => boolean;
53
+
54
+ function createComposite(group: string, id: string, parent: CompositeTreeNode, depth: number, expandable: boolean): Preference.CompositeTreeNode {
55
+ const node: Preference.CompositeTreeNode = {
56
+ id: `${group}@${id}`,
57
+ visible: true,
58
+ parent,
59
+ children: [],
60
+ expanded: false,
61
+ selected: false,
62
+ depth,
63
+ label: id,
64
+ } as Preference.CompositeTreeNode;
65
+ if (!expandable) {
66
+ delete node.expanded;
67
+ }
68
+ CompositeTreeNode.addChild(parent, node);
69
+ return node;
70
+ }
71
+
72
+ function createLeaf(group: string, preferenceId: string, parent: Preference.CompositeTreeNode, depth: number): Preference.LeafNode {
73
+ const node: Preference.LeafNode = {
74
+ id: `${group}@${preferenceId}`,
75
+ preferenceId,
76
+ parent,
77
+ preference: { data: {} },
78
+ depth,
79
+ };
80
+ CompositeTreeNode.addChild(parent, node);
81
+ return node;
82
+ }
83
+
84
+ function createFixture(withCommonlyUsed: boolean = true): TreeFixture {
85
+ const root: CompositeTreeNode = {
86
+ id: 'root-node-id',
87
+ name: '',
88
+ parent: undefined,
89
+ visible: true,
90
+ children: [],
91
+ };
92
+ let commonlyUsed: Preference.CompositeTreeNode | undefined;
93
+ if (withCommonlyUsed) {
94
+ commonlyUsed = createComposite(COMMONLY_USED_SECTION_PREFIX, COMMONLY_USED_SECTION_PREFIX, root, 0, true);
95
+ createLeaf(COMMONLY_USED_SECTION_PREFIX, 'files.autoSave', commonlyUsed, 1);
96
+ }
97
+ const editor = createComposite('editor', 'editor', root, 0, true);
98
+ createLeaf('editor', 'editor.fontSize', editor, 1);
99
+ const editorCursor = createComposite('editor', 'editor.cursor', editor, 1, false);
100
+ createLeaf('editor', 'editor.cursorStyle', editorCursor, 2);
101
+ const extensions = createComposite('extensions', 'extensions', root, 0, true);
102
+ createLeaf('extensions', 'extensions.autoUpdate', extensions, 1);
103
+ return { root, commonlyUsed, editor, editorCursor, extensions };
104
+ }
105
+
106
+ /**
107
+ * Creates the model. The `onCreated` callback runs after the tree model's listeners are
108
+ * registered but before the (async) initial schema is applied, which simulates state that
109
+ * is restored before initialization completes, e.g. a restored search term.
110
+ */
111
+ async function createModel(fixture: TreeFixture, onCreated?: (model: PreferenceTreeModel) => void): Promise<PreferenceTreeModel> {
112
+ const parent = new Container();
113
+ searchbarEmitter = new Emitter<string>();
114
+ schemaEmitter = new Emitter<CompositeTreeNode>();
115
+ scopeEmitter = new Emitter<Preference.SelectedScopeDetails>();
116
+ isValidInScope = () => true;
117
+ parent.bind(ILogger).to(MockLogger).inSingletonScope();
118
+ parent.bind(LabelProvider).toConstantValue({
119
+ getName: () => '',
120
+ getIcon: () => '',
121
+ getLongName: () => '',
122
+ } as any);
123
+ parent.bind(PreferenceSchemaService).toConstantValue({
124
+ getSchemaProperties: () => new Map(),
125
+ isValidInScope: (preferenceId: string, scope: number) => isValidInScope(preferenceId, scope),
126
+ } as any);
127
+ parent.bind(PreferenceService).toConstantValue({
128
+ ready: Promise.resolve(),
129
+ } as any);
130
+ parent.bind(PreferencesSearchbarWidget).toConstantValue({
131
+ onFilterChanged: searchbarEmitter.event,
132
+ updateResultsCount: () => { },
133
+ } as any);
134
+ parent.bind(PreferenceTreeGenerator).toConstantValue({
135
+ onSchemaChanged: schemaEmitter.event,
136
+ root: fixture.root,
137
+ getNodeId: () => '',
138
+ } as any);
139
+ parent.bind(PreferencesScopeTabBar).toConstantValue({
140
+ onScopeChanged: scopeEmitter.event,
141
+ currentScope: Preference.DEFAULT_SCOPE,
142
+ } as any);
143
+ const child = createTreeContainer(parent, { model: PreferenceTreeModel });
144
+ const model = child.get(PreferenceTreeModel);
145
+ onCreated?.(model);
146
+ await new Promise(resolve => setTimeout(resolve));
147
+ return model;
148
+ }
149
+
150
+ describe('category filter', () => {
151
+
152
+ it('selects the Commonly Used category by default', async () => {
153
+ const fixture = createFixture();
154
+ const model = await createModel(fixture);
155
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.commonlyUsed!.id);
156
+ expect(model.categoryFilterId).to.equal(fixture.commonlyUsed!.id);
157
+ });
158
+
159
+ it('applies the category filter when a category is selected', async () => {
160
+ const fixture = createFixture();
161
+ const model = await createModel(fixture);
162
+ model.selectNode(fixture.extensions);
163
+ expect(model.categoryFilterId).to.equal(fixture.extensions.id);
164
+ });
165
+
166
+ it('keeps the user selection across schema reloads', async () => {
167
+ const fixture = createFixture();
168
+ const model = await createModel(fixture);
169
+ model.selectNode(fixture.extensions);
170
+ schemaEmitter.fire(fixture.root);
171
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.extensions.id);
172
+ expect(model.categoryFilterId).to.equal(fixture.extensions.id);
173
+ });
174
+
175
+ it('restores the user selection when the schema is rebuilt with new nodes', async () => {
176
+ const fixture = createFixture();
177
+ const model = await createModel(fixture);
178
+ model.selectNode(fixture.extensions);
179
+ const rebuilt = createFixture();
180
+ schemaEmitter.fire(rebuilt.root);
181
+ expect(model.selectedNodes[0]).to.equal(model.getNode(rebuilt.extensions.id));
182
+ expect(model.selectedNodes[0]?.selected).to.be.true;
183
+ expect(model.categoryFilterId).to.equal(rebuilt.extensions.id);
184
+ });
185
+
186
+ it('keeps the default selection highlighted when the schema is rebuilt with new nodes', async () => {
187
+ const fixture = createFixture();
188
+ const model = await createModel(fixture);
189
+ const rebuilt = createFixture();
190
+ schemaEmitter.fire(rebuilt.root);
191
+ expect(model.selectedNodes[0]).to.equal(model.getNode(rebuilt.commonlyUsed!.id));
192
+ expect(model.selectedNodes[0]?.selected).to.be.true;
193
+ });
194
+
195
+ it('preserves the category filter when restoring a synced selection after a schema rebuild', async () => {
196
+ const fixture = createFixture();
197
+ const model = await createModel(fixture);
198
+ model.selectNode(fixture.editor);
199
+ model.selectIfNotSelected(fixture.editorCursor);
200
+ const rebuilt = createFixture();
201
+ schemaEmitter.fire(rebuilt.root);
202
+ expect(model.selectedNodes[0]).to.equal(model.getNode(rebuilt.editorCursor.id));
203
+ expect(model.selectedNodes[0]?.selected).to.be.true;
204
+ expect(model.categoryFilterId).to.equal(rebuilt.editor.id);
205
+ });
206
+
207
+ it('falls back to the default category when the selected category disappears from the schema', async () => {
208
+ const fixture = createFixture();
209
+ const model = await createModel(fixture);
210
+ model.selectNode(fixture.extensions);
211
+ const newFixture = createFixture();
212
+ newFixture.root.children = newFixture.root.children.filter(child => child !== newFixture.extensions);
213
+ schemaEmitter.fire(newFixture.root);
214
+ expect(model.selectedNodes[0]).to.equal(model.getNode(newFixture.commonlyUsed!.id));
215
+ expect(model.categoryFilterId).to.equal(newFixture.commonlyUsed!.id);
216
+ });
217
+
218
+ it('falls back to the default category when the selected category has no settings left after a schema change', async () => {
219
+ const fixture = createFixture();
220
+ const model = await createModel(fixture);
221
+ model.selectNode(fixture.extensions);
222
+ const rebuilt = createFixture();
223
+ rebuilt.extensions.children = [];
224
+ schemaEmitter.fire(rebuilt.root);
225
+ expect(model.selectedNodes[0]?.id).to.equal(rebuilt.commonlyUsed!.id);
226
+ expect(model.categoryFilterId).to.equal(rebuilt.commonlyUsed!.id);
227
+ });
228
+
229
+ it('selects the Commonly Used category by its exact id', async () => {
230
+ const fixture = createFixture(false);
231
+ // A group whose id merely starts with the Commonly Used prefix must not be mistaken for it.
232
+ const lookalikeGroup = `${COMMONLY_USED_SECTION_PREFIX}-lookalike`;
233
+ const lookalike = createComposite(lookalikeGroup, lookalikeGroup, fixture.root, 0, true);
234
+ createLeaf(lookalikeGroup, 'lookalike.setting', lookalike, 1);
235
+ fixture.commonlyUsed = createComposite(COMMONLY_USED_SECTION_PREFIX, COMMONLY_USED_SECTION_PREFIX, fixture.root, 0, true);
236
+ createLeaf(COMMONLY_USED_SECTION_PREFIX, 'files.autoSave', fixture.commonlyUsed, 1);
237
+ const model = await createModel(fixture);
238
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.commonlyUsed.id);
239
+ expect(model.categoryFilterId).to.equal(fixture.commonlyUsed.id);
240
+ });
241
+ });
242
+
243
+ describe('scope change', () => {
244
+
245
+ function fireScopeChange(scope: number): void {
246
+ scopeEmitter.fire({ scope, uri: undefined, activeScopeIsFolder: false });
247
+ }
248
+
249
+ it('keeps the selected category when it still has settings in the new scope', async () => {
250
+ const fixture = createFixture();
251
+ const model = await createModel(fixture);
252
+ model.selectNode(fixture.extensions);
253
+ fireScopeChange(PreferenceScope.Workspace);
254
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.extensions.id);
255
+ expect(model.categoryFilterId).to.equal(fixture.extensions.id);
256
+ });
257
+
258
+ it('falls back to the default category when the selected category has no settings in the new scope', async () => {
259
+ const fixture = createFixture();
260
+ const model = await createModel(fixture);
261
+ model.selectNode(fixture.extensions);
262
+ isValidInScope = (preferenceId, scope) => scope !== PreferenceScope.Workspace || !preferenceId.startsWith('extensions.');
263
+ fireScopeChange(PreferenceScope.Workspace);
264
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.commonlyUsed!.id);
265
+ expect(model.categoryFilterId).to.equal(fixture.commonlyUsed!.id);
266
+ });
267
+
268
+ it('clears the selection when neither the selected nor the default category has settings in the new scope', async () => {
269
+ const fixture = createFixture();
270
+ const model = await createModel(fixture);
271
+ model.selectNode(fixture.extensions);
272
+ isValidInScope = (preferenceId, scope) => scope !== PreferenceScope.Workspace || preferenceId.startsWith('editor.');
273
+ fireScopeChange(PreferenceScope.Workspace);
274
+ expect(model.selectedNodes).to.have.lengthOf(0);
275
+ expect(model.categoryFilterId).to.be.undefined;
276
+ });
277
+ });
278
+
279
+ describe('editor scroll sync', () => {
280
+
281
+ it('does not change the category filter when the selection syncs to the editor scroll position', async () => {
282
+ const fixture = createFixture();
283
+ const model = await createModel(fixture);
284
+ model.selectNode(fixture.editor);
285
+ model.selectIfNotSelected(fixture.editorCursor);
286
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.editorCursor.id);
287
+ expect(model.categoryFilterId).to.equal(fixture.editor.id);
288
+ });
289
+
290
+ it('does not narrow active search results when the selection syncs to the editor scroll position', async () => {
291
+ const fixture = createFixture();
292
+ const model = await createModel(fixture);
293
+ searchbarEmitter.fire('font');
294
+ model.selectIfNotSelected(fixture.editor);
295
+ expect(model.categoryFilterId).to.be.undefined;
296
+ });
297
+ });
298
+
299
+ describe('search interaction', () => {
300
+
301
+ it('clears the category filter and the selection when a search starts', async () => {
302
+ const fixture = createFixture();
303
+ const model = await createModel(fixture);
304
+ model.selectNode(fixture.editor);
305
+ searchbarEmitter.fire('font');
306
+ expect(model.isFiltered).to.be.true;
307
+ expect(model.categoryFilterId).to.be.undefined;
308
+ expect(model.selectedNodes).to.have.lengthOf(0);
309
+ });
310
+
311
+ it('applies the category filter when a category is explicitly selected during a search', async () => {
312
+ const fixture = createFixture();
313
+ const model = await createModel(fixture);
314
+ searchbarEmitter.fire('font');
315
+ model.selectNode(fixture.editor);
316
+ expect(model.categoryFilterId).to.equal(fixture.editor.id);
317
+ });
318
+
319
+ it('resets to all results when the search term changes after a category was selected', async () => {
320
+ const fixture = createFixture();
321
+ const model = await createModel(fixture);
322
+ searchbarEmitter.fire('font');
323
+ model.selectNode(fixture.editor);
324
+ searchbarEmitter.fire('fonts');
325
+ expect(model.categoryFilterId).to.be.undefined;
326
+ expect(model.selectedNodes).to.have.lengthOf(0);
327
+ });
328
+
329
+ it('returns to the Commonly Used category when the search is cleared', async () => {
330
+ const fixture = createFixture();
331
+ const model = await createModel(fixture);
332
+ searchbarEmitter.fire('font');
333
+ model.selectNode(fixture.editor);
334
+ searchbarEmitter.fire('');
335
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.commonlyUsed!.id);
336
+ expect(model.categoryFilterId).to.equal(fixture.commonlyUsed!.id);
337
+ });
338
+ });
339
+
340
+ describe('initial selection', () => {
341
+
342
+ it('does not apply the default selection while a restored search is active', async () => {
343
+ const fixture = createFixture();
344
+ const model = await createModel(fixture, () => searchbarEmitter.fire('font'));
345
+ expect(model.selectedNodes).to.have.lengthOf(0);
346
+ expect(model.categoryFilterId).to.be.undefined;
347
+ searchbarEmitter.fire('');
348
+ expect(model.selectedNodes[0]?.id).to.equal(fixture.commonlyUsed!.id);
349
+ expect(model.categoryFilterId).to.equal(fixture.commonlyUsed!.id);
350
+ });
351
+
352
+ it('applies the default selection on a later schema change if Commonly Used is not present initially', async () => {
353
+ const withoutCommonlyUsed = createFixture(false);
354
+ const model = await createModel(withoutCommonlyUsed);
355
+ expect(model.selectedNodes).to.have.lengthOf(0);
356
+ const withCommonlyUsed = createFixture();
357
+ schemaEmitter.fire(withCommonlyUsed.root);
358
+ expect(model.selectedNodes[0]?.id).to.equal(withCommonlyUsed.commonlyUsed!.id);
359
+ expect(model.categoryFilterId).to.equal(withCommonlyUsed.commonlyUsed!.id);
360
+ });
361
+ });
362
+ });