Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts
5263 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as DOM from '../../../../base/browser/dom.js';
7
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
9
import * as aria from '../../../../base/browser/ui/aria/aria.js';
10
import { Button } from '../../../../base/browser/ui/button/button.js';
11
import { Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js';
12
import { ToggleActionViewItem } from '../../../../base/browser/ui/toggle/toggle.js';
13
import { ITreeElement } from '../../../../base/browser/ui/tree/tree.js';
14
import { CodeWindow } from '../../../../base/browser/window.js';
15
import { Action } from '../../../../base/common/actions.js';
16
import { CancelablePromise, createCancelablePromise, Delayer, raceTimeout } from '../../../../base/common/async.js';
17
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
18
import { Color } from '../../../../base/common/color.js';
19
import { fromNow } from '../../../../base/common/date.js';
20
import { isCancellationError } from '../../../../base/common/errors.js';
21
import { Emitter, Event } from '../../../../base/common/event.js';
22
import { Iterable } from '../../../../base/common/iterator.js';
23
import { KeyCode } from '../../../../base/common/keyCodes.js';
24
import { Disposable, DisposableStore, dispose, type IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
25
import * as platform from '../../../../base/common/platform.js';
26
import { StopWatch } from '../../../../base/common/stopwatch.js';
27
import { ThemeIcon } from '../../../../base/common/themables.js';
28
import { URI } from '../../../../base/common/uri.js';
29
import { ILanguageService } from '../../../../editor/common/languages/language.js';
30
import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';
31
import { localize } from '../../../../nls.js';
32
import { ICommandService } from '../../../../platform/commands/common/commands.js';
33
import { ConfigurationTarget, IConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js';
34
import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
35
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
36
import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
37
import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
38
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
39
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
40
import { ILogService } from '../../../../platform/log/common/log.js';
41
import { IProductService } from '../../../../platform/product/common/productService.js';
42
import { IEditorProgressService, IProgressRunner } from '../../../../platform/progress/common/progress.js';
43
import { Registry } from '../../../../platform/registry/common/platform.js';
44
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
45
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
46
import { defaultButtonStyles, defaultToggleStyles } from '../../../../platform/theme/browser/defaultStyles.js';
47
import { asCssVariable, asCssVariableWithDefault, badgeBackground, badgeForeground, contrastBorder, editorForeground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js';
48
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
49
import { IUserDataSyncEnablementService, IUserDataSyncService, SyncStatus } from '../../../../platform/userDataSync/common/userDataSync.js';
50
import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';
51
import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';
52
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
53
import { IEditorMemento, IEditorOpenContext, IEditorPane } from '../../../common/editor.js';
54
import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js';
55
import { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js';
56
import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
57
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
58
import { ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING, IOpenSettingsOptions, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsEditorOptions, ISettingsGroup, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from '../../../services/preferences/common/preferences.js';
59
import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js';
60
import { nullRange, Settings2EditorModel } from '../../../services/preferences/common/preferencesModels.js';
61
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
62
import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js';
63
import { SuggestEnabledInput } from '../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js';
64
import { ADVANCED_SETTING_TAG, CONTEXT_AI_SETTING_RESULTS_AVAILABLE, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, EMBEDDINGS_SEARCH_PROVIDER_NAME, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, FILTER_MODEL_SEARCH_PROVIDER_NAME, getExperimentalExtensionToggleData, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, LLM_RANKED_SEARCH_PROVIDER_NAME, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_AI_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, SETTINGS_EDITOR_COMMAND_TOGGLE_AI_SEARCH, STRING_MATCH_SEARCH_PROVIDER_NAME, TF_IDF_SEARCH_PROVIDER_NAME, WorkbenchSettingsEditorSettings, WORKSPACE_TRUST_SETTING_TAG } from '../common/preferences.js';
65
import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js';
66
import './media/settingsEditor2.css';
67
import { preferencesAiResultsIcon, preferencesClearInputIcon, preferencesFilterIcon } from './preferencesIcons.js';
68
import { SettingsTarget, SettingsTargetsWidget } from './preferencesWidgets.js';
69
import { ISettingOverrideClickEvent } from './settingsEditorSettingIndicators.js';
70
import { getCommonlyUsedData, ITOCEntry, tocData } from './settingsLayout.js';
71
import { SettingsSearchFilterDropdownMenuActionViewItem } from './settingsSearchMenu.js';
72
import { AbstractSettingRenderer, createTocTreeForExtensionSettings, HeightChangeParams, ISettingLinkClickEvent, resolveConfiguredUntrustedSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from './settingsTree.js';
73
import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from './settingsTreeModels.js';
74
import { createTOCIterator, TOCTree, TOCTreeModel } from './tocTree.js';
75
76
export const enum SettingsFocusContext {
77
Search,
78
TableOfContents,
79
SettingTree,
80
SettingControl
81
}
82
83
export function createGroupIterator(group: SettingsTreeGroupElement): Iterable<ITreeElement<SettingsTreeGroupChild>> {
84
return Iterable.map(group.children, g => {
85
return {
86
element: g,
87
children: g instanceof SettingsTreeGroupElement ?
88
createGroupIterator(g) :
89
undefined
90
};
91
});
92
}
93
94
const $ = DOM.$;
95
96
interface IFocusEventFromScroll extends KeyboardEvent {
97
fromScroll: true;
98
}
99
100
const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings");
101
const SEARCH_TOC_BEHAVIOR_KEY = 'workbench.settings.settingsSearchTocBehavior';
102
const SCROLL_BEHAVIOR_KEY = 'workbench.settings.scrollBehavior';
103
104
const SHOW_AI_RESULTS_ENABLED_LABEL = localize('showAiResultsEnabled', "Show AI-recommended results");
105
const SHOW_AI_RESULTS_DISABLED_LABEL = localize('showAiResultsDisabled', "No AI results available at this time...");
106
107
const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState';
108
109
export class SettingsEditor2 extends EditorPane {
110
111
static readonly ID: string = 'workbench.editor.settings2';
112
private static NUM_INSTANCES: number = 0;
113
private static SEARCH_DEBOUNCE: number = 200;
114
private static SETTING_UPDATE_FAST_DEBOUNCE: number = 200;
115
private static SETTING_UPDATE_SLOW_DEBOUNCE: number = 1000;
116
private static CONFIG_SCHEMA_UPDATE_DELAYER = 500;
117
private static TOC_MIN_WIDTH: number = 100;
118
private static TOC_RESET_WIDTH: number = 200;
119
private static EDITOR_MIN_WIDTH: number = 500;
120
// Below NARROW_TOTAL_WIDTH, we only render the editor rather than the ToC.
121
private static NARROW_TOTAL_WIDTH: number = this.TOC_RESET_WIDTH + this.EDITOR_MIN_WIDTH;
122
123
private static SUGGESTIONS: string[] = [
124
`@${MODIFIED_SETTING_TAG}`,
125
'@tag:notebookLayout',
126
'@tag:notebookOutputLayout',
127
`@tag:${REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG}`,
128
`@tag:${WORKSPACE_TRUST_SETTING_TAG}`,
129
'@tag:sync',
130
'@tag:usesOnlineServices',
131
'@tag:telemetry',
132
'@tag:accessibility',
133
'@tag:preview',
134
'@tag:experimental',
135
`@tag:${ADVANCED_SETTING_TAG}`,
136
`@${ID_SETTING_TAG}`,
137
`@${EXTENSION_SETTING_TAG}`,
138
`@${FEATURE_SETTING_TAG}scm`,
139
`@${FEATURE_SETTING_TAG}explorer`,
140
`@${FEATURE_SETTING_TAG}search`,
141
`@${FEATURE_SETTING_TAG}debug`,
142
`@${FEATURE_SETTING_TAG}extensions`,
143
`@${FEATURE_SETTING_TAG}terminal`,
144
`@${FEATURE_SETTING_TAG}task`,
145
`@${FEATURE_SETTING_TAG}problems`,
146
`@${FEATURE_SETTING_TAG}output`,
147
`@${FEATURE_SETTING_TAG}comments`,
148
`@${FEATURE_SETTING_TAG}remote`,
149
`@${FEATURE_SETTING_TAG}timeline`,
150
`@${FEATURE_SETTING_TAG}notebook`,
151
`@${FEATURE_SETTING_TAG}chat`,
152
`@${POLICY_SETTING_TAG}`
153
];
154
155
private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean {
156
if (Array.isArray(type)) {
157
// nullable integer/number or complex
158
return false;
159
}
160
return type === SettingValueType.Enum ||
161
type === SettingValueType.Array ||
162
type === SettingValueType.BooleanObject ||
163
type === SettingValueType.Object ||
164
type === SettingValueType.Complex ||
165
type === SettingValueType.Boolean ||
166
type === SettingValueType.Exclude ||
167
type === SettingValueType.Include;
168
}
169
170
// (!) Lots of props that are set once on the first render
171
private defaultSettingsEditorModel!: Settings2EditorModel;
172
private readonly modelDisposables: DisposableStore;
173
174
private rootElement!: HTMLElement;
175
private headerContainer!: HTMLElement;
176
private searchContainer: HTMLElement | null = null;
177
private bodyContainer!: HTMLElement;
178
private searchWidget!: SuggestEnabledInput;
179
private countElement!: HTMLElement;
180
private controlsElement!: HTMLElement;
181
private settingsTargetsWidget!: SettingsTargetsWidget;
182
183
private splitView!: SplitView<number>;
184
185
private settingsTreeContainer!: HTMLElement;
186
private settingsTree!: SettingsTree;
187
private settingRenderers!: SettingTreeRenderers;
188
private tocTreeModel!: TOCTreeModel;
189
private readonly settingsTreeModel = this._register(new MutableDisposable<SettingsTreeModel>());
190
private noResultsMessage!: HTMLElement;
191
private clearFilterLinkContainer!: HTMLElement;
192
193
private tocTreeContainer!: HTMLElement;
194
private tocTree!: TOCTree;
195
196
private searchDelayer: Delayer<void>;
197
private searchInProgress: CancellationTokenSource | null = null;
198
private aiSearchPromise: CancelablePromise<void> | null = null;
199
200
private stopWatch: StopWatch;
201
202
private showAiResultsAction: Action | null = null;
203
204
private searchInputDelayer: Delayer<void>;
205
private updatedConfigSchemaDelayer: Delayer<void>;
206
207
private settingFastUpdateDelayer: Delayer<void>;
208
private settingSlowUpdateDelayer: Delayer<void>;
209
private pendingSettingUpdate: { key: string; value: unknown; languageFilter: string | undefined } | null = null;
210
211
private readonly viewState: ISettingsEditorViewState;
212
private readonly _searchResultModel = this._register(new MutableDisposable<SearchResultModel>());
213
private searchResultLabel: string | null = null;
214
private lastSyncedLabel: string | null = null;
215
private settingsOrderByTocIndex: Map<string, number> | null = null;
216
217
private tocRowFocused: IContextKey<boolean>;
218
private settingRowFocused: IContextKey<boolean>;
219
private inSettingsEditorContextKey: IContextKey<boolean>;
220
private searchFocusContextKey: IContextKey<boolean>;
221
private aiResultsAvailable: IContextKey<boolean>;
222
223
private scheduledRefreshes: Map<string, DisposableStore>;
224
private _currentFocusContext: SettingsFocusContext = SettingsFocusContext.Search;
225
226
/** Don't spam warnings */
227
private hasWarnedMissingSettings = false;
228
private tocTreeDisposed = false;
229
230
/** Persist the search query upon reloads */
231
private editorMemento: IEditorMemento<ISettingsEditor2State>;
232
233
private tocFocusedElement: SettingsTreeGroupElement | null = null;
234
private treeFocusedElement: SettingsTreeElement | null = null;
235
private settingsTreeScrollTop = 0;
236
private dimension!: DOM.Dimension;
237
238
private installedExtensionIds: string[] = [];
239
private dismissedExtensionSettings: string[] = [];
240
241
private readonly DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY = 'settingsEditor2.dismissedExtensionSettings';
242
private readonly DISMISSED_EXTENSION_SETTINGS_DELIMITER = '\t';
243
244
private readonly inputChangeListener: MutableDisposable<IDisposable>;
245
246
private searchInputActionBar: ActionBar | null = null;
247
248
constructor(
249
group: IEditorGroup,
250
@ITelemetryService telemetryService: ITelemetryService,
251
@IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService,
252
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
253
@IThemeService themeService: IThemeService,
254
@IPreferencesService private readonly preferencesService: IPreferencesService,
255
@IInstantiationService private readonly instantiationService: IInstantiationService,
256
@IPreferencesSearchService private readonly preferencesSearchService: IPreferencesSearchService,
257
@ILogService private readonly logService: ILogService,
258
@IContextKeyService contextKeyService: IContextKeyService,
259
@IStorageService private readonly storageService: IStorageService,
260
@IEditorGroupsService protected editorGroupService: IEditorGroupsService,
261
@IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService,
262
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
263
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
264
@IExtensionService private readonly extensionService: IExtensionService,
265
@ILanguageService private readonly languageService: ILanguageService,
266
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
267
@IProductService private readonly productService: IProductService,
268
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
269
@IEditorProgressService private readonly editorProgressService: IEditorProgressService,
270
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
271
@IKeybindingService private readonly keybindingService: IKeybindingService,
272
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService
273
) {
274
super(SettingsEditor2.ID, group, telemetryService, themeService, storageService);
275
this.searchDelayer = this._register(new Delayer(200));
276
this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL };
277
278
this.settingFastUpdateDelayer = this._register(new Delayer<void>(SettingsEditor2.SETTING_UPDATE_FAST_DEBOUNCE));
279
this.settingSlowUpdateDelayer = this._register(new Delayer<void>(SettingsEditor2.SETTING_UPDATE_SLOW_DEBOUNCE));
280
281
this.searchInputDelayer = this._register(new Delayer<void>(SettingsEditor2.SEARCH_DEBOUNCE));
282
this.updatedConfigSchemaDelayer = this._register(new Delayer<void>(SettingsEditor2.CONFIG_SCHEMA_UPDATE_DELAYER));
283
284
this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);
285
this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);
286
this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService);
287
this.settingRowFocused = CONTEXT_SETTINGS_ROW_FOCUS.bindTo(contextKeyService);
288
this.aiResultsAvailable = CONTEXT_AI_SETTING_RESULTS_AVAILABLE.bindTo(contextKeyService);
289
290
this.scheduledRefreshes = new Map<string, DisposableStore>();
291
this.stopWatch = new StopWatch(false);
292
293
this.editorMemento = this.getEditorMemento<ISettingsEditor2State>(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY);
294
295
this.dismissedExtensionSettings = this.storageService
296
.get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '')
297
.split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER);
298
299
this._register(configurationService.onDidChangeConfiguration(e => {
300
if (e.affectedKeys.has(WorkbenchSettingsEditorSettings.ShowAISearchToggle)
301
|| e.affectedKeys.has(WorkbenchSettingsEditorSettings.EnableNaturalLanguageSearch)) {
302
this.updateAiSearchToggleVisibility();
303
}
304
if (e.affectsConfiguration(ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING)) {
305
this.onConfigUpdate(undefined, true, true);
306
}
307
if (e.source !== ConfigurationTarget.DEFAULT) {
308
this.onConfigUpdate(e.affectedKeys);
309
}
310
}));
311
312
this._register(chatEntitlementService.onDidChangeSentiment(() => {
313
this.updateAiSearchToggleVisibility();
314
}));
315
316
this._register(userDataProfileService.onDidChangeCurrentProfile(e => {
317
e.join(this.whenCurrentProfileChanged());
318
}));
319
320
this._register(workspaceTrustManagementService.onDidChangeTrust(() => {
321
this.searchResultModel?.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());
322
323
if (this.settingsTreeModel.value) {
324
this.settingsTreeModel.value.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());
325
this.renderTree();
326
}
327
}));
328
329
this._register(configurationService.onDidChangeRestrictedSettings(e => {
330
if (e.default.length && this.currentSettingsModel) {
331
this.updateElementsByKey(new Set(e.default));
332
}
333
}));
334
335
this._register(extensionManagementService.onDidInstallExtensions(() => {
336
this.refreshInstalledExtensionsList();
337
}));
338
this._register(extensionManagementService.onDidUninstallExtension(() => {
339
this.refreshInstalledExtensionsList();
340
}));
341
342
this.modelDisposables = this._register(new DisposableStore());
343
344
if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) {
345
SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`);
346
}
347
this.inputChangeListener = this._register(new MutableDisposable());
348
}
349
350
private async whenCurrentProfileChanged(): Promise<void> {
351
this.updatedConfigSchemaDelayer.trigger(() => {
352
this.dismissedExtensionSettings = this.storageService
353
.get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '')
354
.split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER);
355
this.onConfigUpdate(undefined, true);
356
});
357
}
358
359
private canShowAdvancedSettings(): boolean {
360
if (this.configurationService.getValue<boolean>(ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING) ?? false) {
361
return true;
362
}
363
return this.viewState.tagFilters?.has(ADVANCED_SETTING_TAG) ?? false;
364
}
365
366
/**
367
* Determines whether a setting should be shown even when advanced settings are filtered out.
368
* Returns true if:
369
* - The setting is not tagged as advanced, OR
370
* - The setting matches an ID filter (@id:settingKey), OR
371
* - The setting key appears in the search query, OR
372
* - The @hasPolicy filter is active (policy settings should always be shown when filtering by policy)
373
*/
374
private shouldShowSetting(setting: ISetting): boolean {
375
if (!setting.tags?.includes(ADVANCED_SETTING_TAG)) {
376
return true;
377
}
378
if (this.viewState.idFilters?.has(setting.key)) {
379
return true;
380
}
381
if (this.viewState.query?.toLowerCase().includes(setting.key.toLowerCase())) {
382
return true;
383
}
384
if (this.viewState.tagFilters?.has(POLICY_SETTING_TAG)) {
385
return true;
386
}
387
return false;
388
}
389
390
private disableAiSearchToggle(): void {
391
if (this.showAiResultsAction) {
392
this.showAiResultsAction.checked = false;
393
this.showAiResultsAction.enabled = false;
394
this.aiResultsAvailable.set(false);
395
this.showAiResultsAction.label = SHOW_AI_RESULTS_DISABLED_LABEL;
396
}
397
}
398
399
private updateAiSearchToggleVisibility(): void {
400
if (!this.searchContainer || !this.showAiResultsAction || !this.searchInputActionBar) {
401
return;
402
}
403
404
const showAiToggle = this.configurationService.getValue<boolean>(WorkbenchSettingsEditorSettings.ShowAISearchToggle);
405
const enableNaturalLanguageSearch = this.configurationService.getValue<boolean>(WorkbenchSettingsEditorSettings.EnableNaturalLanguageSearch);
406
const chatHidden = this.chatEntitlementService.sentiment.hidden || this.chatEntitlementService.sentiment.disabled;
407
const canShowToggle = showAiToggle && enableNaturalLanguageSearch && !chatHidden;
408
409
const alreadyVisible = this.searchInputActionBar.hasAction(this.showAiResultsAction);
410
if (!alreadyVisible && canShowToggle) {
411
this.searchInputActionBar.push(this.showAiResultsAction, {
412
index: 0,
413
label: false,
414
icon: true
415
});
416
this.searchContainer.classList.add('with-ai-toggle');
417
} else if (alreadyVisible) {
418
this.searchInputActionBar.pull(0);
419
this.searchContainer.classList.remove('with-ai-toggle');
420
this.showAiResultsAction.checked = false;
421
}
422
}
423
424
override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; }
425
override get maximumWidth(): number { return Number.POSITIVE_INFINITY; }
426
override get minimumHeight() { return 180; }
427
428
// these setters need to exist because this extends from EditorPane
429
override set minimumWidth(value: number) { /*noop*/ }
430
override set maximumWidth(value: number) { /*noop*/ }
431
432
private get currentSettingsModel(): SettingsTreeModel | undefined {
433
return this.searchResultModel || this.settingsTreeModel.value;
434
}
435
436
private get searchResultModel(): SearchResultModel | null {
437
return this._searchResultModel.value ?? null;
438
}
439
440
private set searchResultModel(value: SearchResultModel | null) {
441
this._searchResultModel.value = value ?? undefined;
442
443
this.rootElement.classList.toggle('search-mode', !!this._searchResultModel.value);
444
}
445
446
private get focusedSettingDOMElement(): HTMLElement | undefined {
447
const focused = this.settingsTree.getFocus()[0];
448
if (!(focused instanceof SettingsTreeSettingElement)) {
449
return;
450
}
451
452
return this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), focused.setting.key)[0];
453
}
454
455
get currentFocusContext() {
456
return this._currentFocusContext;
457
}
458
459
protected createEditor(parent: HTMLElement): void {
460
parent.setAttribute('tabindex', '-1');
461
this.rootElement = DOM.append(parent, $('.settings-editor', { tabindex: '-1' }));
462
463
this.createHeader(this.rootElement);
464
this.createBody(this.rootElement);
465
this.addCtrlAInterceptor(this.rootElement);
466
this.updateStyles();
467
468
this._register(registerNavigableContainer({
469
name: 'settingsEditor2',
470
focusNotifiers: [this],
471
focusNextWidget: () => {
472
if (this.searchWidget.inputWidget.hasWidgetFocus()) {
473
this.focusTOC();
474
}
475
},
476
focusPreviousWidget: () => {
477
if (!this.searchWidget.inputWidget.hasWidgetFocus()) {
478
this.focusSearch();
479
}
480
}
481
}));
482
}
483
484
override async setInput(input: SettingsEditor2Input, options: ISettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
485
this.inSettingsEditorContextKey.set(true);
486
await super.setInput(input, options, context, token);
487
if (!this.input) {
488
return;
489
}
490
491
const model = await this.input.resolve();
492
if (token.isCancellationRequested || !(model instanceof Settings2EditorModel)) {
493
return;
494
}
495
496
this.modelDisposables.clear();
497
this.modelDisposables.add(model.onDidChangeGroups(() => {
498
this.updatedConfigSchemaDelayer.trigger(() => {
499
this.onConfigUpdate(undefined, false, true);
500
});
501
}));
502
this.defaultSettingsEditorModel = model;
503
504
options = options || validateSettingsEditorOptions({});
505
if (!this.viewState.settingsTarget || !this.settingsTargetsWidget.settingsTarget) {
506
const optionsHasViewStateTarget = options.viewState && (options.viewState as ISettingsEditorViewState).settingsTarget;
507
if (!options.target && !optionsHasViewStateTarget) {
508
options.target = ConfigurationTarget.USER_LOCAL;
509
}
510
}
511
this._setOptions(options);
512
513
// Don't block setInput on render (which can trigger an async search)
514
this.onConfigUpdate(undefined, true).then(() => {
515
// This event runs when the editor closes.
516
this.inputChangeListener.value = input.onWillDispose(() => {
517
this.searchWidget.setValue('');
518
});
519
520
// Init TOC selection
521
this.updateTreeScrollSync();
522
});
523
524
await this.refreshInstalledExtensionsList();
525
}
526
527
private async refreshInstalledExtensionsList(): Promise<void> {
528
const installedExtensions = await this.extensionManagementService.getInstalled();
529
this.installedExtensionIds = installedExtensions
530
.filter(ext => ext.manifest.contributes?.configuration)
531
.map(ext => ext.identifier.id);
532
}
533
534
private restoreCachedState(): ISettingsEditor2State | null {
535
const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input);
536
if (cachedState && typeof cachedState.target === 'object') {
537
cachedState.target = URI.revive(cachedState.target);
538
}
539
540
if (cachedState) {
541
const settingsTarget = cachedState.target;
542
this.settingsTargetsWidget.settingsTarget = settingsTarget;
543
this.viewState.settingsTarget = settingsTarget;
544
if (!this.searchWidget.getValue()) {
545
this.searchWidget.setValue(cachedState.searchQuery);
546
}
547
}
548
549
if (this.input) {
550
this.editorMemento.clearEditorState(this.input, this.group);
551
}
552
553
return cachedState ?? null;
554
}
555
556
override getViewState(): object | undefined {
557
return this.viewState;
558
}
559
560
override setOptions(options: ISettingsEditorOptions | undefined): void {
561
super.setOptions(options);
562
563
if (options) {
564
this._setOptions(options);
565
}
566
}
567
568
private _setOptions(options: ISettingsEditorOptions): void {
569
if (options.focusSearch && !platform.isIOS) {
570
// isIOS - #122044
571
this.focusSearch();
572
}
573
574
const recoveredViewState = options.viewState ?
575
options.viewState as ISettingsEditorViewState : undefined;
576
577
const query: string | undefined = recoveredViewState?.query ?? options.query;
578
if (query !== undefined) {
579
this.searchWidget.setValue(query);
580
this.viewState.query = query;
581
}
582
583
const target: SettingsTarget | undefined = options.folderUri ?? recoveredViewState?.settingsTarget ?? <SettingsTarget | undefined>options.target;
584
if (target) {
585
this.settingsTargetsWidget.updateTarget(target);
586
}
587
}
588
589
override clearInput(): void {
590
this.inSettingsEditorContextKey.set(false);
591
super.clearInput();
592
}
593
594
layout(dimension: DOM.Dimension): void {
595
this.dimension = dimension;
596
597
if (!this.isVisible()) {
598
return;
599
}
600
601
this.layoutSplitView(dimension);
602
603
const innerWidth = Math.min(this.headerContainer.clientWidth, dimension.width) - 24 * 2; // 24px padding on left and right;
604
// minus padding inside inputbox, controls width, and extra padding before countElement
605
const monacoWidth = innerWidth - 10 - this.controlsElement.clientWidth - 12;
606
this.searchWidget.layout(new DOM.Dimension(monacoWidth, 20));
607
608
this.rootElement.classList.toggle('narrow-width', dimension.width < SettingsEditor2.NARROW_TOTAL_WIDTH);
609
}
610
611
override focus(): void {
612
super.focus();
613
614
if (this._currentFocusContext === SettingsFocusContext.Search) {
615
if (!platform.isIOS) {
616
// #122044
617
this.focusSearch();
618
}
619
} else if (this._currentFocusContext === SettingsFocusContext.SettingControl) {
620
const element = this.focusedSettingDOMElement;
621
if (element) {
622
// eslint-disable-next-line no-restricted-syntax
623
const control = element.querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
624
if (control) {
625
(<HTMLElement>control).focus();
626
return;
627
}
628
}
629
} else if (this._currentFocusContext === SettingsFocusContext.SettingTree) {
630
this.settingsTree.domFocus();
631
} else if (this._currentFocusContext === SettingsFocusContext.TableOfContents) {
632
this.tocTree.domFocus();
633
}
634
}
635
636
protected override setEditorVisible(visible: boolean): void {
637
super.setEditorVisible(visible);
638
639
if (!visible) {
640
// Wait for editor to be removed from DOM #106303
641
setTimeout(() => {
642
this.searchWidget.onHide();
643
this.settingRenderers.cancelSuggesters();
644
}, 0);
645
}
646
}
647
648
focusSettings(focusSettingInput = false): void {
649
const focused = this.settingsTree.getFocus();
650
if (!focused.length) {
651
this.settingsTree.focusFirst();
652
}
653
654
this.settingsTree.domFocus();
655
656
if (focusSettingInput) {
657
// eslint-disable-next-line no-restricted-syntax
658
const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`);
659
if (controlInFocusedRow) {
660
(<HTMLElement>controlInFocusedRow).focus();
661
}
662
}
663
}
664
665
focusTOC(): void {
666
this.tocTree.domFocus();
667
}
668
669
showContextMenu(): void {
670
const focused = this.settingsTree.getFocus()[0];
671
const rowElement = this.focusedSettingDOMElement;
672
if (rowElement && focused instanceof SettingsTreeSettingElement) {
673
this.settingRenderers.showContextMenu(focused, rowElement);
674
}
675
}
676
677
focusSearch(filter?: string, selectAll = true): void {
678
if (filter && this.searchWidget) {
679
this.searchWidget.setValue(filter);
680
}
681
682
// Do not select all if the user is already searching.
683
this.searchWidget.focus(selectAll && !this.searchInputDelayer.isTriggered);
684
}
685
686
clearSearchResults(): void {
687
this.disableAiSearchToggle();
688
this.searchWidget.setValue('');
689
this.focusSearch();
690
}
691
692
clearSearchFilters(): void {
693
const query = this.searchWidget.getValue();
694
695
const splitQuery = query.split(' ').filter(word => {
696
return word.length && !SettingsEditor2.SUGGESTIONS.some(suggestion => word.startsWith(suggestion));
697
});
698
699
this.searchWidget.setValue(splitQuery.join(' '));
700
}
701
702
private updateInputAriaLabel() {
703
let label = searchBoxLabel;
704
if (this.searchResultLabel) {
705
label += `. ${this.searchResultLabel}`;
706
}
707
708
if (this.lastSyncedLabel) {
709
label += `. ${this.lastSyncedLabel}`;
710
}
711
712
this.searchWidget.updateAriaLabel(label);
713
}
714
715
/**
716
* Render the header of the Settings editor, which includes the content above the splitview.
717
*/
718
private createHeader(parent: HTMLElement): void {
719
this.headerContainer = DOM.append(parent, $('.settings-header'));
720
this.searchContainer = DOM.append(this.headerContainer, $('.search-container'));
721
722
const clearInputAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS,
723
localize('clearInput', "Clear Settings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false,
724
async () => this.clearSearchResults()
725
));
726
727
const showAiResultActionClassNames = ['action-label', ThemeIcon.asClassName(preferencesAiResultsIcon)];
728
this.showAiResultsAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_SHOW_AI_RESULTS,
729
SHOW_AI_RESULTS_DISABLED_LABEL, showAiResultActionClassNames.join(' '), true
730
));
731
this._register(this.showAiResultsAction.onDidChange(async () => {
732
await this.onDidToggleAiSearch();
733
}));
734
735
const filterAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS,
736
localize('filterInput', "Filter Settings"), ThemeIcon.asClassName(preferencesFilterIcon)
737
));
738
739
this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, this.searchContainer, {
740
triggerCharacters: ['@', ':'],
741
provideResults: (query: string) => {
742
// Based on testing, the trigger character is always at the end of the query.
743
// for the ':' trigger, only return suggestions if there was a '@' before it in the same word.
744
const queryParts = query.split(/\s/g);
745
if (queryParts[queryParts.length - 1].startsWith(`@${LANGUAGE_SETTING_TAG}`)) {
746
const sortedLanguages = this.languageService.getRegisteredLanguageIds().map(languageId => {
747
return `@${LANGUAGE_SETTING_TAG}${languageId} `;
748
}).sort();
749
return sortedLanguages.filter(langFilter => !query.includes(langFilter));
750
} else if (queryParts[queryParts.length - 1].startsWith(`@${EXTENSION_SETTING_TAG}`)) {
751
const installedExtensionsTags = this.installedExtensionIds.map(extensionId => {
752
return `@${EXTENSION_SETTING_TAG}${extensionId} `;
753
}).sort();
754
return installedExtensionsTags.filter(extFilter => !query.includes(extFilter));
755
} else if (query === '' || queryParts[queryParts.length - 1].startsWith('@')) {
756
return SettingsEditor2.SUGGESTIONS.filter(tag => !query.includes(tag)).map(tag => tag.endsWith(':') ? tag : tag + ' ');
757
}
758
return [];
759
}
760
}, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, {
761
placeholderText: searchBoxLabel,
762
focusContextKey: this.searchFocusContextKey,
763
styleOverrides: {
764
inputBorder: settingsTextInputBorder
765
}
766
// TODO: Aria-live
767
}));
768
this._register(this.searchWidget.onDidFocus(() => {
769
this._currentFocusContext = SettingsFocusContext.Search;
770
}));
771
this._register(this.searchWidget.onInputDidChange(() => {
772
const searchVal = this.searchWidget.getValue();
773
clearInputAction.enabled = !!searchVal;
774
this.searchInputDelayer.trigger(() => this.onSearchInputChanged(true));
775
}));
776
777
const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
778
headerControlsContainer.style.borderColor = asCssVariable(settingsHeaderBorder);
779
780
const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
781
this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true }));
782
this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL;
783
this._register(this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target)));
784
this._register(DOM.addDisposableListener(targetWidgetContainer, DOM.EventType.KEY_DOWN, e => {
785
const event = new StandardKeyboardEvent(e);
786
if (event.keyCode === KeyCode.DownArrow) {
787
this.focusSettings();
788
}
789
}));
790
791
const headerRightControlsContainer = DOM.append(headerControlsContainer, $('.settings-right-controls'));
792
793
const openSettingsJsonContainer = DOM.append(headerRightControlsContainer, $('.open-settings-json'));
794
const openSettingsJsonButton = this._register(new Button(openSettingsJsonContainer, { secondary: true, title: true, ...defaultButtonStyles }));
795
openSettingsJsonButton.label = localize('openSettingsJson', "Edit as JSON");
796
this._register(openSettingsJsonButton.onDidClick(() => this.openSettingsFile()));
797
798
if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) {
799
const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerRightControlsContainer));
800
this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => {
801
this.lastSyncedLabel = lastSyncedLabel;
802
this.updateInputAriaLabel();
803
}));
804
}
805
806
this.controlsElement = DOM.append(this.searchContainer, DOM.$('.search-container-widgets'));
807
808
this.countElement = DOM.append(this.controlsElement, DOM.$('.settings-count-widget.monaco-count-badge.long'));
809
this.countElement.style.backgroundColor = asCssVariable(badgeBackground);
810
this.countElement.style.color = asCssVariable(badgeForeground);
811
this.countElement.style.border = `1px solid ${asCssVariableWithDefault(contrastBorder, asCssVariable(inputBackground))}`;
812
813
this.searchInputActionBar = this._register(new ActionBar(this.controlsElement, {
814
actionViewItemProvider: (action, options) => {
815
if (action.id === filterAction.id) {
816
return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, options, this.actionRunner, this.searchWidget);
817
}
818
if (this.showAiResultsAction && action.id === this.showAiResultsAction.id) {
819
const keybindingLabel = this.keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_TOGGLE_AI_SEARCH)?.getLabel();
820
return new ToggleActionViewItem(null, action, { ...options, keybinding: keybindingLabel, toggleStyles: defaultToggleStyles });
821
}
822
return undefined;
823
}
824
}));
825
826
const actionsToPush = [clearInputAction, filterAction];
827
this.searchInputActionBar.push(actionsToPush, { label: false, icon: true });
828
829
this.disableAiSearchToggle();
830
this.updateAiSearchToggleVisibility();
831
}
832
833
toggleAiSearch(): void {
834
if (this.searchInputActionBar && this.showAiResultsAction && this.searchInputActionBar.hasAction(this.showAiResultsAction)) {
835
if (!this.showAiResultsAction.enabled) {
836
aria.status(localize('noAiResults', "No AI results available at this time."));
837
}
838
this.showAiResultsAction.checked = !this.showAiResultsAction.checked;
839
}
840
}
841
842
private async onDidToggleAiSearch(): Promise<void> {
843
if (this.searchResultModel && this.showAiResultsAction) {
844
this.searchResultModel.showAiResults = this.showAiResultsAction.checked ?? false;
845
this.renderResultCountMessages(false);
846
this.onDidFinishSearch(true, undefined);
847
}
848
}
849
850
private onDidSettingsTargetChange(target: SettingsTarget): void {
851
this.viewState.settingsTarget = target;
852
853
// TODO Instead of rebuilding the whole model, refresh and uncache the inspected setting value
854
this.onConfigUpdate(undefined, true);
855
}
856
857
private onDidDismissExtensionSetting(extensionId: string): void {
858
if (!this.dismissedExtensionSettings.includes(extensionId)) {
859
this.dismissedExtensionSettings.push(extensionId);
860
}
861
this.storageService.store(
862
this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY,
863
this.dismissedExtensionSettings.join(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER),
864
StorageScope.PROFILE,
865
StorageTarget.USER
866
);
867
this.onConfigUpdate(undefined, true);
868
}
869
870
private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void {
871
// eslint-disable-next-line no-restricted-syntax
872
const targetElement = this.currentSettingsModel?.getElementsByName(evt.targetKey)?.[0];
873
let revealFailed = false;
874
if (targetElement) {
875
let sourceTop = 0.5;
876
try {
877
const _sourceTop = this.settingsTree.getRelativeTop(evt.source);
878
if (_sourceTop !== null) {
879
sourceTop = _sourceTop;
880
}
881
} catch {
882
// e.g. clicked a searched element, now the search has been cleared
883
}
884
885
// If we search for something and focus on a category, the settings tree
886
// only renders settings in that category.
887
// If the target display category is different than the source's, unfocus the category
888
// so that we can render all found settings again.
889
// Then, the reveal call will correctly find the target setting.
890
if (this.viewState.categoryFilter && evt.source.displayCategory !== targetElement.displayCategory) {
891
this.tocTree.setFocus([]);
892
}
893
try {
894
this.settingsTree.reveal(targetElement, sourceTop);
895
} catch (_) {
896
// The listwidget couldn't find the setting to reveal,
897
// even though it's in the model, meaning there might be a filter
898
// preventing it from showing up.
899
revealFailed = true;
900
}
901
902
if (!revealFailed) {
903
// We need to shift focus from the setting that contains the link to the setting that's
904
// linked. Clicking on the link sets focus on the setting that contains the link,
905
// which is why we need the setTimeout.
906
setTimeout(() => {
907
this.settingsTree.setFocus([targetElement]);
908
}, 50);
909
910
const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey);
911
if (domElements && domElements[0]) {
912
// eslint-disable-next-line no-restricted-syntax
913
const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
914
if (control) {
915
(<HTMLElement>control).focus();
916
}
917
}
918
}
919
}
920
921
if (!recursed && (!targetElement || revealFailed)) {
922
// We'll call this event handler again after clearing the search query,
923
// so that more settings show up in the list.
924
const p = this.triggerSearch('', true);
925
p.then(() => {
926
this.searchWidget.setValue('');
927
this.onDidClickSetting(evt, true);
928
});
929
}
930
}
931
932
switchToSettingsFile(): Promise<IEditorPane | undefined> {
933
const query = parseQuery(this.searchWidget.getValue()).query;
934
return this.openSettingsFile({ query });
935
}
936
937
private async openSettingsFile(options?: ISettingsEditorOptions): Promise<IEditorPane | undefined> {
938
const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget;
939
940
const openOptions: IOpenSettingsOptions = { jsonEditor: true, groupId: this.group.id, ...options };
941
if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) {
942
if (options?.revealSetting) {
943
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
944
const configurationScope = configurationProperties[options?.revealSetting.key]?.scope;
945
if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) {
946
return this.preferencesService.openApplicationSettings(openOptions);
947
}
948
}
949
return this.preferencesService.openUserSettings(openOptions);
950
} else if (currentSettingsTarget === ConfigurationTarget.USER_REMOTE) {
951
return this.preferencesService.openRemoteSettings(openOptions);
952
} else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) {
953
return this.preferencesService.openWorkspaceSettings(openOptions);
954
} else if (URI.isUri(currentSettingsTarget)) {
955
return this.preferencesService.openFolderSettings({ folderUri: currentSettingsTarget, ...openOptions });
956
}
957
958
return undefined;
959
}
960
961
private createBody(parent: HTMLElement): void {
962
this.bodyContainer = DOM.append(parent, $('.settings-body'));
963
964
this.noResultsMessage = DOM.append(this.bodyContainer, $('.no-results-message'));
965
966
this.noResultsMessage.innerText = localize('noResults', "No Settings Found");
967
968
this.clearFilterLinkContainer = $('span.clear-search-filters');
969
970
this.clearFilterLinkContainer.textContent = ' - ';
971
const clearFilterLink = DOM.append(this.clearFilterLinkContainer, $('a.pointer.prominent', { tabindex: 0 }, localize('clearSearchFilters', 'Clear Filters')));
972
this._register(DOM.addDisposableListener(clearFilterLink, DOM.EventType.CLICK, (e: MouseEvent) => {
973
DOM.EventHelper.stop(e, false);
974
this.clearSearchFilters();
975
}));
976
977
DOM.append(this.noResultsMessage, this.clearFilterLinkContainer);
978
979
this.noResultsMessage.style.color = asCssVariable(editorForeground);
980
981
this.tocTreeContainer = $('.settings-toc-container');
982
this.settingsTreeContainer = $('.settings-tree-container');
983
984
this.createTOC(this.tocTreeContainer);
985
this.createSettingsTree(this.settingsTreeContainer);
986
987
this.splitView = this._register(new SplitView(this.bodyContainer, {
988
orientation: Orientation.HORIZONTAL,
989
proportionalLayout: true
990
}));
991
const startingWidth = this.storageService.getNumber('settingsEditor2.splitViewWidth', StorageScope.PROFILE, SettingsEditor2.TOC_RESET_WIDTH);
992
this.splitView.addView({
993
onDidChange: Event.None,
994
element: this.tocTreeContainer,
995
minimumSize: SettingsEditor2.TOC_MIN_WIDTH,
996
maximumSize: Number.POSITIVE_INFINITY,
997
layout: (width, _, height) => {
998
this.tocTreeContainer.style.width = `${width}px`;
999
this.tocTree.layout(height, width);
1000
}
1001
}, startingWidth, undefined, true);
1002
this.splitView.addView({
1003
onDidChange: Event.None,
1004
element: this.settingsTreeContainer,
1005
minimumSize: SettingsEditor2.EDITOR_MIN_WIDTH,
1006
maximumSize: Number.POSITIVE_INFINITY,
1007
layout: (width, _, height) => {
1008
this.settingsTreeContainer.style.width = `${width}px`;
1009
this.settingsTree.layout(height, width);
1010
}
1011
}, Sizing.Distribute, undefined, true);
1012
this._register(this.splitView.onDidSashReset(() => {
1013
const totalSize = this.splitView.getViewSize(0) + this.splitView.getViewSize(1);
1014
this.splitView.resizeView(0, SettingsEditor2.TOC_RESET_WIDTH);
1015
this.splitView.resizeView(1, totalSize - SettingsEditor2.TOC_RESET_WIDTH);
1016
}));
1017
this._register(this.splitView.onDidSashChange(() => {
1018
const width = this.splitView.getViewSize(0);
1019
this.storageService.store('settingsEditor2.splitViewWidth', width, StorageScope.PROFILE, StorageTarget.USER);
1020
}));
1021
const borderColor = this.theme.getColor(settingsSashBorder)!;
1022
this.splitView.style({ separatorBorder: borderColor });
1023
}
1024
1025
private addCtrlAInterceptor(container: HTMLElement): void {
1026
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
1027
if (
1028
e.keyCode === KeyCode.KeyA &&
1029
(platform.isMacintosh ? e.metaKey : e.ctrlKey) &&
1030
!DOM.isEditableElement(e.target)
1031
) {
1032
// Avoid browser ctrl+a
1033
e.browserEvent.stopPropagation();
1034
e.browserEvent.preventDefault();
1035
}
1036
}));
1037
}
1038
1039
private createTOC(container: HTMLElement): void {
1040
this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState);
1041
1042
this.tocTree = this._register(this.instantiationService.createInstance(TOCTree,
1043
DOM.append(container, $('.settings-toc-wrapper', {
1044
'role': 'navigation',
1045
'aria-label': localize('settings', "Settings"),
1046
})),
1047
this.viewState));
1048
this.tocTreeDisposed = false;
1049
1050
this._register(this.tocTree.onDidFocus(() => {
1051
this._currentFocusContext = SettingsFocusContext.TableOfContents;
1052
}));
1053
1054
this._register(this.tocTree.onDidChangeFocus(e => {
1055
const element: SettingsTreeGroupElement | null = e.elements?.[0] ?? null;
1056
if (this.tocFocusedElement === element) {
1057
return;
1058
}
1059
1060
this.tocFocusedElement = element;
1061
this.tocTree.setSelection(element ? [element] : []);
1062
const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY);
1063
if (this.searchResultModel || scrollBehavior === 'paginated') {
1064
// In search mode or paginated mode, filter to show only the selected category
1065
if (this.viewState.categoryFilter !== element) {
1066
this.viewState.categoryFilter = element ?? undefined;
1067
// Force render in this case, because
1068
// onDidClickSetting relies on the updated view.
1069
this.renderTree(undefined, true);
1070
this.settingsTree.scrollTop = 0;
1071
}
1072
} else {
1073
// In continuous mode, clear any category filter that may have been set in paginated mode
1074
if (this.viewState.categoryFilter) {
1075
this.viewState.categoryFilter = undefined;
1076
this.renderTree(undefined, true);
1077
}
1078
if (element && (!e.browserEvent || !(<IFocusEventFromScroll>e.browserEvent).fromScroll)) {
1079
let targetElement = element;
1080
// Searches equivalent old Object currently living in the Tree nodes.
1081
if (!this.settingsTree.hasElement(targetElement)) {
1082
if (element instanceof SettingsTreeGroupElement) {
1083
const targetId = element.id;
1084
1085
const findInViewNodes = (nodes: any[]): SettingsTreeGroupElement | undefined => {
1086
for (const node of nodes) {
1087
if (node.element instanceof SettingsTreeGroupElement && node.element.id === targetId) {
1088
return node.element;
1089
}
1090
if (node.children && node.children.length > 0) {
1091
const found = findInViewNodes(node.children);
1092
if (found) {
1093
return found;
1094
}
1095
}
1096
}
1097
return undefined;
1098
};
1099
1100
try {
1101
const rootNode = this.settingsTree.getNode(null);
1102
if (rootNode && rootNode.children) {
1103
const foundOldElement = findInViewNodes(rootNode.children);
1104
if (foundOldElement) {
1105
// Now we don't reveal the New Object, reveal the Old Object"
1106
targetElement = foundOldElement;
1107
}
1108
}
1109
} catch (err) {
1110
// Tree might be in an invalid state, ignore
1111
}
1112
}
1113
}
1114
1115
if (this.settingsTree.hasElement(targetElement)) {
1116
this.settingsTree.reveal(targetElement, 0);
1117
this.settingsTree.setFocus([targetElement]);
1118
}
1119
}
1120
}
1121
}));
1122
1123
this._register(this.tocTree.onDidFocus(() => {
1124
this.tocRowFocused.set(true);
1125
}));
1126
1127
this._register(this.tocTree.onDidBlur(() => {
1128
this.tocRowFocused.set(false);
1129
}));
1130
1131
this._register(this.tocTree.onDidDispose(() => {
1132
this.tocTreeDisposed = true;
1133
}));
1134
}
1135
1136
private applyFilter(filter: string) {
1137
if (this.searchWidget && !this.searchWidget.getValue().includes(filter)) {
1138
// Prepend the filter to the query.
1139
const newQuery = `${filter} ${this.searchWidget.getValue().trimStart()}`;
1140
this.focusSearch(newQuery, false);
1141
}
1142
}
1143
1144
private removeLanguageFilters() {
1145
if (this.searchWidget && this.searchWidget.getValue().includes(`@${LANGUAGE_SETTING_TAG}`)) {
1146
const query = this.searchWidget.getValue().split(' ');
1147
const newQuery = query.filter(word => !word.startsWith(`@${LANGUAGE_SETTING_TAG}`)).join(' ');
1148
this.focusSearch(newQuery, false);
1149
}
1150
}
1151
1152
private createSettingsTree(container: HTMLElement): void {
1153
this.settingRenderers = this._register(this.instantiationService.createInstance(SettingTreeRenderers));
1154
this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope)));
1155
this._register(this.settingRenderers.onDidDismissExtensionSetting((e) => this.onDidDismissExtensionSetting(e)));
1156
this._register(this.settingRenderers.onDidOpenSettings(settingKey => {
1157
this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } });
1158
}));
1159
this._register(this.settingRenderers.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName)));
1160
this._register(this.settingRenderers.onDidFocusSetting(element => {
1161
this.settingsTree.setFocus([element]);
1162
this._currentFocusContext = SettingsFocusContext.SettingControl;
1163
this.settingRowFocused.set(false);
1164
}));
1165
this._register(this.settingRenderers.onDidChangeSettingHeight((params: HeightChangeParams) => {
1166
const { element, height } = params;
1167
try {
1168
this.settingsTree.updateElementHeight(element, height);
1169
} catch (e) {
1170
// the element was not found
1171
}
1172
}));
1173
this._register(this.settingRenderers.onApplyFilter((filter) => this.applyFilter(filter)));
1174
this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => {
1175
this.removeLanguageFilters();
1176
if (element.language) {
1177
this.applyFilter(`@${LANGUAGE_SETTING_TAG}${element.language}`);
1178
}
1179
1180
if (element.scope === 'workspace') {
1181
this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE);
1182
} else if (element.scope === 'user') {
1183
this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL);
1184
} else if (element.scope === 'remote') {
1185
this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE);
1186
}
1187
this.applyFilter(`@${ID_SETTING_TAG}${element.settingKey}`);
1188
}));
1189
1190
this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree,
1191
container,
1192
this.viewState,
1193
this.settingRenderers.allRenderers));
1194
1195
this._register(this.settingsTree.onDidScroll(() => {
1196
if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) {
1197
return;
1198
}
1199
1200
this.settingsTreeScrollTop = this.settingsTree.scrollTop;
1201
1202
// setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when
1203
// setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync
1204
setTimeout(() => {
1205
this.updateTreeScrollSync();
1206
}, 0);
1207
}));
1208
1209
this._register(this.settingsTree.onDidFocus(() => {
1210
const classList = container.ownerDocument.activeElement?.classList;
1211
if (classList && classList.contains('monaco-list') && classList.contains('settings-editor-tree')) {
1212
this._currentFocusContext = SettingsFocusContext.SettingTree;
1213
this.settingRowFocused.set(true);
1214
this.treeFocusedElement ??= this.settingsTree.firstVisibleElement ?? null;
1215
if (this.treeFocusedElement) {
1216
this.treeFocusedElement.tabbable = true;
1217
}
1218
}
1219
}));
1220
1221
this._register(this.settingsTree.onDidBlur(() => {
1222
this.settingRowFocused.set(false);
1223
// Clear out the focused element, otherwise it could be
1224
// out of date during the next onDidFocus event.
1225
this.treeFocusedElement = null;
1226
}));
1227
1228
// There is no different select state in the settings tree
1229
this._register(this.settingsTree.onDidChangeFocus(e => {
1230
const element = e.elements[0];
1231
if (this.treeFocusedElement === element) {
1232
return;
1233
}
1234
1235
if (this.treeFocusedElement) {
1236
this.treeFocusedElement.tabbable = false;
1237
}
1238
1239
this.treeFocusedElement = element;
1240
1241
if (this.treeFocusedElement) {
1242
this.treeFocusedElement.tabbable = true;
1243
}
1244
1245
this.settingsTree.setSelection(element ? [element] : []);
1246
}));
1247
}
1248
1249
private onDidChangeSetting(key: string, value: unknown, type: SettingValueType | SettingValueType[], manualReset: boolean, scope: ConfigurationScope | undefined): void {
1250
const parsedQuery = parseQuery(this.searchWidget.getValue());
1251
const languageFilter = parsedQuery.languageFilter;
1252
if (manualReset || (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key)) {
1253
this.updateChangedSetting(key, value, manualReset, languageFilter, scope);
1254
}
1255
1256
this.pendingSettingUpdate = { key, value, languageFilter };
1257
if (SettingsEditor2.shouldSettingUpdateFast(type)) {
1258
this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope));
1259
} else {
1260
this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope));
1261
}
1262
}
1263
1264
private updateTreeScrollSync(): void {
1265
this.settingRenderers.cancelSuggesters();
1266
if (this.searchResultModel) {
1267
return;
1268
}
1269
1270
// In paginated mode, we don't sync scroll position since categories are filtered
1271
const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY);
1272
if (scrollBehavior === 'paginated') {
1273
return;
1274
}
1275
1276
if (!this.tocTreeModel) {
1277
return;
1278
}
1279
1280
const elementToSync = this.settingsTree.firstVisibleElement;
1281
const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent :
1282
elementToSync instanceof SettingsTreeGroupElement ? elementToSync :
1283
null;
1284
1285
// It's possible for this to be called when the TOC and settings tree are out of sync - e.g. when the settings tree has deferred a refresh because
1286
// it is focused. So, bail if element doesn't exist in the TOC.
1287
let nodeExists = true;
1288
try { this.tocTree.getNode(element); } catch (e) { nodeExists = false; }
1289
if (!nodeExists) {
1290
return;
1291
}
1292
1293
if (element && this.tocTree.getSelection()[0] !== element) {
1294
const ancestors = this.getAncestors(element);
1295
ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));
1296
1297
this.tocTree.reveal(element);
1298
const elementTop = this.tocTree.getRelativeTop(element);
1299
if (typeof elementTop !== 'number') {
1300
return;
1301
}
1302
1303
this.tocTree.collapseAll();
1304
1305
ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));
1306
if (elementTop < 0 || elementTop > 1) {
1307
this.tocTree.reveal(element);
1308
} else {
1309
this.tocTree.reveal(element, elementTop);
1310
}
1311
1312
this.tocTree.expand(element);
1313
1314
this.tocTree.setSelection([element]);
1315
1316
const fakeKeyboardEvent = new KeyboardEvent('keydown');
1317
(<IFocusEventFromScroll>fakeKeyboardEvent).fromScroll = true;
1318
this.tocTree.setFocus([element], fakeKeyboardEvent);
1319
}
1320
}
1321
1322
private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] {
1323
const ancestors: SettingsTreeElement[] = [];
1324
1325
while (element.parent) {
1326
if (element.parent.id !== 'root') {
1327
ancestors.push(element.parent);
1328
}
1329
1330
element = element.parent;
1331
}
1332
1333
return ancestors.reverse();
1334
}
1335
1336
private updateChangedSetting(key: string, value: unknown, manualReset: boolean, languageFilter: string | undefined, scope: ConfigurationScope | undefined): Promise<void> {
1337
// ConfigurationService displays the error if this fails.
1338
// Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change.
1339
const settingsTarget = this.settingsTargetsWidget.settingsTarget;
1340
const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined;
1341
const configurationTarget = <ConfigurationTarget | null>(resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget) ?? ConfigurationTarget.USER_LOCAL;
1342
const overrides: IConfigurationUpdateOverrides = { resource, overrideIdentifiers: languageFilter ? [languageFilter] : undefined };
1343
1344
const configurationTargetIsWorkspace = configurationTarget === ConfigurationTarget.WORKSPACE || configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER;
1345
1346
const userPassedInManualReset = configurationTargetIsWorkspace || !!languageFilter;
1347
const isManualReset = userPassedInManualReset ? manualReset : value === undefined;
1348
1349
// If the user is changing the value back to the default, and we're not targeting a workspace scope, do a 'reset' instead
1350
const inspected = this.configurationService.inspect(key, overrides);
1351
if (!userPassedInManualReset && inspected.defaultValue === value) {
1352
value = undefined;
1353
}
1354
1355
return this.configurationService.updateValue(key, value, overrides, configurationTarget, { handleDirtyFile: 'save' })
1356
.then(() => {
1357
const query = this.searchWidget.getValue();
1358
if (query.includes(`@${MODIFIED_SETTING_TAG}`)) {
1359
// The user might have reset a setting.
1360
this.refreshTOCTree();
1361
}
1362
this.renderTree(key, isManualReset);
1363
this.pendingSettingUpdate = null;
1364
1365
const reportModifiedProps = {
1366
key,
1367
query,
1368
searchResults: this.searchResultModel?.getUniqueSearchResults() ?? null,
1369
rawResults: this.searchResultModel?.getRawResults() ?? null,
1370
showConfiguredOnly: !!this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG),
1371
isReset: typeof value === 'undefined',
1372
settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget
1373
};
1374
return this.reportModifiedSetting(reportModifiedProps);
1375
});
1376
}
1377
1378
private reportModifiedSetting(props: { key: string; query: string; searchResults: ISearchResult | null; rawResults: ISearchResult[] | null; showConfiguredOnly: boolean; isReset: boolean; settingsTarget: SettingsTarget }): void {
1379
type SettingsEditorModifiedSettingEvent = {
1380
key: string;
1381
groupId: string | undefined;
1382
providerName: string | undefined;
1383
nlpIndex: number | undefined;
1384
displayIndex: number | undefined;
1385
showConfiguredOnly: boolean;
1386
isReset: boolean;
1387
target: string;
1388
};
1389
type SettingsEditorModifiedSettingClassification = {
1390
key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The setting that is being modified.' };
1391
groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' };
1392
providerName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the search provider, if applicable.' };
1393
nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The index of the setting in the remote search provider results, if applicable.' };
1394
displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The index of the setting in the combined search results, if applicable.' };
1395
showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is in the modified view, which shows configured settings only.' };
1396
isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Identifies whether a setting was reset to its default value.' };
1397
target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The scope of the setting, such as user or workspace.' };
1398
owner: 'rzhao271';
1399
comment: 'Event emitted when the user modifies a setting in the Settings editor';
1400
};
1401
1402
let groupId: string | undefined = undefined;
1403
let providerName: string | undefined = undefined;
1404
let nlpIndex: number | undefined = undefined;
1405
let displayIndex: number | undefined = undefined;
1406
if (props.searchResults) {
1407
displayIndex = props.searchResults.filterMatches.findIndex(m => m.setting.key === props.key);
1408
1409
if (this.searchResultModel) {
1410
providerName = props.searchResults.filterMatches.find(m => m.setting.key === props.key)?.providerName;
1411
const rawResults = this.searchResultModel.getRawResults();
1412
if (rawResults[SearchResultIdx.Local] && displayIndex >= 0) {
1413
const settingInLocalResults = rawResults[SearchResultIdx.Local].filterMatches.some(m => m.setting.key === props.key);
1414
groupId = settingInLocalResults ? 'local' : 'remote';
1415
}
1416
if (rawResults[SearchResultIdx.Remote]) {
1417
const _nlpIndex = rawResults[SearchResultIdx.Remote].filterMatches.findIndex(m => m.setting.key === props.key);
1418
nlpIndex = _nlpIndex >= 0 ? _nlpIndex : undefined;
1419
}
1420
}
1421
}
1422
1423
const reportedTarget = props.settingsTarget === ConfigurationTarget.USER_LOCAL ? 'user' :
1424
props.settingsTarget === ConfigurationTarget.USER_REMOTE ? 'user_remote' :
1425
props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' :
1426
'folder';
1427
1428
const data = {
1429
key: props.key,
1430
groupId,
1431
providerName,
1432
nlpIndex,
1433
displayIndex,
1434
showConfiguredOnly: props.showConfiguredOnly,
1435
isReset: props.isReset,
1436
target: reportedTarget
1437
};
1438
1439
this.telemetryService.publicLog2<SettingsEditorModifiedSettingEvent, SettingsEditorModifiedSettingClassification>('settingsEditor.settingModified', data);
1440
}
1441
1442
private scheduleRefresh(element: HTMLElement, key = ''): void {
1443
if (key && this.scheduledRefreshes.has(key)) {
1444
return;
1445
}
1446
1447
if (!key) {
1448
dispose(this.scheduledRefreshes.values());
1449
this.scheduledRefreshes.clear();
1450
}
1451
1452
const store = new DisposableStore();
1453
const scheduledRefreshTracker = DOM.trackFocus(element);
1454
store.add(scheduledRefreshTracker);
1455
store.add(scheduledRefreshTracker.onDidBlur(() => {
1456
this.scheduledRefreshes.get(key)?.dispose();
1457
this.scheduledRefreshes.delete(key);
1458
this.onConfigUpdate(new Set([key]));
1459
}));
1460
this.scheduledRefreshes.set(key, store);
1461
}
1462
1463
private createSettingsOrderByTocIndex(resolvedSettingsRoot: ITOCEntry<ISetting>): Map<string, number> {
1464
const index = new Map<string, number>();
1465
function indexSettings(resolvedSettingsRoot: ITOCEntry<ISetting>, counter = 0): number {
1466
if (resolvedSettingsRoot.settings) {
1467
for (const setting of resolvedSettingsRoot.settings) {
1468
if (!index.has(setting.key)) {
1469
index.set(setting.key, counter++);
1470
}
1471
}
1472
}
1473
if (resolvedSettingsRoot.children) {
1474
for (const child of resolvedSettingsRoot.children) {
1475
counter = indexSettings(child, counter);
1476
}
1477
}
1478
return counter;
1479
}
1480
indexSettings(resolvedSettingsRoot);
1481
return index;
1482
}
1483
1484
private refreshModels(resolvedSettingsRoot: ITOCEntry<ISetting>) {
1485
// Both calls to refreshModels require a valid settingsTreeModel.
1486
this.settingsTreeModel.value!.update(resolvedSettingsRoot);
1487
this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.value!.root;
1488
this.settingsOrderByTocIndex = this.createSettingsOrderByTocIndex(resolvedSettingsRoot);
1489
}
1490
1491
private async onConfigUpdate(keys?: ReadonlySet<string>, forceRefresh = false, triggerSearch = false): Promise<void> {
1492
if (keys && this.settingsTreeModel) {
1493
return this.updateElementsByKey(keys);
1494
}
1495
1496
if (!this.defaultSettingsEditorModel) {
1497
return;
1498
}
1499
1500
const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
1501
const coreSettingsGroups = [], extensionSettingsGroups = [];
1502
for (const group of groups) {
1503
if (group.extensionInfo) {
1504
extensionSettingsGroups.push(group);
1505
} else {
1506
coreSettingsGroups.push(group);
1507
}
1508
}
1509
const filter = this.canShowAdvancedSettings() ? undefined : { exclude: { tags: [ADVANCED_SETTING_TAG] } };
1510
1511
const settingsResult = resolveSettingsTree(tocData, coreSettingsGroups, filter, this.logService);
1512
const resolvedSettingsRoot = settingsResult.tree;
1513
1514
// Warn for settings not included in layout
1515
if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) {
1516
const settingKeyList: string[] = [];
1517
settingsResult.leftoverSettings.forEach(s => {
1518
settingKeyList.push(s.key);
1519
});
1520
1521
this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`);
1522
this.hasWarnedMissingSettings = true;
1523
}
1524
1525
const additionalGroups: ISettingsGroup[] = [];
1526
let setAdditionalGroups = false;
1527
const toggleData = await getExperimentalExtensionToggleData(this.chatEntitlementService, this.extensionGalleryService, this.productService);
1528
if (toggleData && groups.filter(g => g.extensionInfo).length) {
1529
for (const key in toggleData.settingsEditorRecommendedExtensions) {
1530
const extension: IGalleryExtension = toggleData.recommendedExtensionsGalleryInfo[key];
1531
if (!extension) {
1532
continue;
1533
}
1534
1535
const extensionId = extension.identifier.id;
1536
// prevent race between extension update handler and this (onConfigUpdate) handler
1537
await this.refreshInstalledExtensionsList();
1538
const extensionInstalled = this.installedExtensionIds.includes(extensionId);
1539
1540
// Drill down to see whether the group and setting already exist
1541
// and need to be removed.
1542
const matchingGroupIndex = groups.findIndex(g =>
1543
g.extensionInfo && g.extensionInfo!.id.toLowerCase() === extensionId.toLowerCase() &&
1544
g.sections.length === 1 && g.sections[0].settings.length === 1 && g.sections[0].settings[0].displayExtensionId
1545
);
1546
if (extensionInstalled || this.dismissedExtensionSettings.includes(extensionId)) {
1547
if (matchingGroupIndex !== -1) {
1548
groups.splice(matchingGroupIndex, 1);
1549
setAdditionalGroups = true;
1550
}
1551
continue;
1552
}
1553
1554
if (matchingGroupIndex !== -1) {
1555
continue;
1556
}
1557
1558
// Create the entry. extensionInstalled is false in this case.
1559
let manifest: IExtensionManifest | null = null;
1560
try {
1561
manifest = await raceTimeout(
1562
this.extensionGalleryService.getManifest(extension, CancellationToken.None),
1563
EXTENSION_FETCH_TIMEOUT_MS
1564
) ?? null;
1565
} catch (e) {
1566
// Likely a networking issue.
1567
// Skip adding a button for this extension to the Settings editor.
1568
continue;
1569
}
1570
1571
if (manifest === null) {
1572
continue;
1573
}
1574
1575
const contributesConfiguration = manifest?.contributes?.configuration;
1576
1577
let groupTitle: string | undefined;
1578
if (!Array.isArray(contributesConfiguration)) {
1579
groupTitle = contributesConfiguration?.title;
1580
} else if (contributesConfiguration.length === 1) {
1581
groupTitle = contributesConfiguration[0].title;
1582
}
1583
1584
const recommendationInfo = toggleData.settingsEditorRecommendedExtensions[key];
1585
const extensionName = extension.displayName ?? extension.name ?? extensionId;
1586
const settingKey = `${key}.manageExtension`;
1587
const setting: ISetting = {
1588
range: nullRange,
1589
key: settingKey,
1590
keyRange: nullRange,
1591
value: null,
1592
valueRange: nullRange,
1593
description: [recommendationInfo.onSettingsEditorOpen?.descriptionOverride ?? extension.description],
1594
descriptionIsMarkdown: false,
1595
descriptionRanges: [],
1596
scope: ConfigurationScope.WINDOW,
1597
type: 'null',
1598
displayExtensionId: extensionId,
1599
extensionGroupTitle: groupTitle ?? extensionName,
1600
categoryLabel: 'Extensions',
1601
title: extensionName
1602
};
1603
const additionalGroup: ISettingsGroup = {
1604
sections: [{
1605
settings: [setting],
1606
}],
1607
id: extensionId,
1608
title: setting.extensionGroupTitle!,
1609
titleRange: nullRange,
1610
range: nullRange,
1611
extensionInfo: {
1612
id: extensionId,
1613
displayName: extension.displayName,
1614
}
1615
};
1616
groups.push(additionalGroup);
1617
additionalGroups.push(additionalGroup);
1618
setAdditionalGroups = true;
1619
}
1620
}
1621
1622
resolvedSettingsRoot.children!.push(await createTocTreeForExtensionSettings(this.extensionService, extensionSettingsGroups, filter));
1623
1624
resolvedSettingsRoot.children!.unshift(getCommonlyUsedData(groups));
1625
1626
if (toggleData && setAdditionalGroups) {
1627
// Add the additional groups to the model to help with searching.
1628
this.defaultSettingsEditorModel.setAdditionalGroups(additionalGroups);
1629
}
1630
1631
if (!this.workspaceTrustManagementService.isWorkspaceTrusted() && (this.viewState.settingsTarget instanceof URI || this.viewState.settingsTarget === ConfigurationTarget.WORKSPACE)) {
1632
const configuredUntrustedWorkspaceSettings = resolveConfiguredUntrustedSettings(groups, this.viewState.settingsTarget, this.viewState.languageFilter, this.configurationService);
1633
if (configuredUntrustedWorkspaceSettings.length) {
1634
resolvedSettingsRoot.children!.unshift({
1635
id: 'workspaceTrust',
1636
label: localize('settings require trust', "Workspace Trust"),
1637
settings: configuredUntrustedWorkspaceSettings
1638
});
1639
}
1640
}
1641
1642
this.searchResultModel?.updateChildren();
1643
1644
const firstVisibleElement = this.settingsTree.firstVisibleElement;
1645
let anchorId: string | undefined;
1646
1647
if (firstVisibleElement instanceof SettingsTreeSettingElement) {
1648
anchorId = firstVisibleElement.setting.key;
1649
} else if (firstVisibleElement instanceof SettingsTreeGroupElement) {
1650
anchorId = firstVisibleElement.id;
1651
}
1652
1653
if (this.settingsTreeModel.value) {
1654
this.refreshModels(resolvedSettingsRoot);
1655
1656
if (triggerSearch && this.searchResultModel) {
1657
// If an extension's settings were just loaded and a search is active, retrigger the search so it shows up
1658
return await this.onSearchInputChanged(false);
1659
}
1660
1661
this.refreshTOCTree();
1662
this.renderTree(undefined, forceRefresh);
1663
1664
if (anchorId) {
1665
const newModel = this.settingsTreeModel.value;
1666
let newElement: SettingsTreeElement | undefined;
1667
1668
// eslint-disable-next-line no-restricted-syntax
1669
const settings = newModel.getElementsByName(anchorId);
1670
if (settings && settings.length > 0) {
1671
newElement = settings[0];
1672
} else {
1673
const findGroup = (roots: SettingsTreeGroupElement[]): SettingsTreeGroupElement | undefined => {
1674
for (const g of roots) {
1675
if (g.id === anchorId) {
1676
return g;
1677
}
1678
if (g.children) {
1679
for (const child of g.children) {
1680
if (child instanceof SettingsTreeGroupElement) {
1681
const found = findGroup([child]);
1682
if (found) {
1683
return found;
1684
}
1685
}
1686
}
1687
}
1688
}
1689
return undefined;
1690
};
1691
newElement = findGroup([newModel.root]);
1692
}
1693
1694
if (newElement) {
1695
try {
1696
this.settingsTree.reveal(newElement, 0);
1697
} catch (e) {
1698
// Ignore the error
1699
}
1700
}
1701
}
1702
} else {
1703
this.settingsTreeModel.value = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());
1704
this.refreshModels(resolvedSettingsRoot);
1705
1706
// Don't restore the cached state if we already have a query value from calling _setOptions().
1707
const cachedState = !this.viewState.query ? this.restoreCachedState() : undefined;
1708
if (cachedState?.searchQuery || this.searchWidget.getValue()) {
1709
await this.onSearchInputChanged(true);
1710
} else {
1711
this.refreshTOCTree();
1712
1713
// In paginated mode, set initial category to the first one (Commonly Used)
1714
const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY);
1715
if (scrollBehavior === 'paginated') {
1716
const rootChildren = this.settingsTreeModel.value.root.children;
1717
if (Array.isArray(rootChildren) && rootChildren.length > 0) {
1718
const firstCategory = rootChildren[0];
1719
if (firstCategory instanceof SettingsTreeGroupElement) {
1720
this.viewState.categoryFilter = firstCategory;
1721
this.tocTree.setFocus([firstCategory]);
1722
this.tocTree.setSelection([firstCategory]);
1723
}
1724
}
1725
}
1726
1727
this.refreshTree();
1728
this.tocTree.collapseAll();
1729
}
1730
}
1731
}
1732
1733
private updateElementsByKey(keys: ReadonlySet<string>): void {
1734
if (keys.size) {
1735
if (this.searchResultModel) {
1736
keys.forEach(key => this.searchResultModel!.updateElementsByName(key));
1737
}
1738
1739
if (this.settingsTreeModel.value) {
1740
keys.forEach(key => this.settingsTreeModel.value!.updateElementsByName(key));
1741
}
1742
1743
keys.forEach(key => this.renderTree(key));
1744
} else {
1745
this.renderTree();
1746
}
1747
}
1748
1749
private getActiveControlInSettingsTree(): HTMLElement | null {
1750
const element = this.settingsTree.getHTMLElement();
1751
const activeElement = element.ownerDocument.activeElement;
1752
return (activeElement && DOM.isAncestorOfActiveElement(element)) ?
1753
<HTMLElement>activeElement :
1754
null;
1755
}
1756
1757
private renderTree(key?: string, force = false): void {
1758
if (!force && key && this.scheduledRefreshes.has(key)) {
1759
this.updateModifiedLabelForKey(key);
1760
return;
1761
}
1762
1763
// If the context view is focused, delay rendering settings
1764
if (this.contextViewFocused()) {
1765
// eslint-disable-next-line no-restricted-syntax
1766
const element = this.window.document.querySelector('.context-view');
1767
if (element) {
1768
this.scheduleRefresh(element as HTMLElement, key);
1769
}
1770
return;
1771
}
1772
1773
// If a setting control is currently focused, schedule a refresh for later
1774
const activeElement = this.getActiveControlInSettingsTree();
1775
const focusedSetting = activeElement && this.settingRenderers.getSettingDOMElementForDOMElement(activeElement);
1776
if (focusedSetting && !force) {
1777
// If a single setting is being refreshed, it's ok to refresh now if that is not the focused setting
1778
if (key) {
1779
const focusedKey = focusedSetting.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR);
1780
if (focusedKey === key &&
1781
// update `list`s live, as they have a separate "submit edit" step built in before this
1782
(focusedSetting.parentElement && !focusedSetting.parentElement.classList.contains('setting-item-list'))
1783
) {
1784
this.updateModifiedLabelForKey(key);
1785
this.scheduleRefresh(focusedSetting, key);
1786
return;
1787
}
1788
} else {
1789
this.scheduleRefresh(focusedSetting);
1790
return;
1791
}
1792
}
1793
1794
this.renderResultCountMessages(false);
1795
1796
if (key) {
1797
// eslint-disable-next-line no-restricted-syntax
1798
const elements = this.currentSettingsModel?.getElementsByName(key);
1799
if (elements?.length) {
1800
if (elements.length >= 2) {
1801
console.warn('More than one setting with key ' + key + ' found');
1802
}
1803
this.refreshSingleElement(elements[0]);
1804
} else {
1805
// Refresh requested for a key that we don't know about
1806
return;
1807
}
1808
} else {
1809
this.refreshTree();
1810
}
1811
1812
return;
1813
}
1814
1815
private contextViewFocused(): boolean {
1816
return !!DOM.findParentWithClass(<HTMLElement>this.rootElement.ownerDocument.activeElement, 'context-view');
1817
}
1818
1819
private refreshSingleElement(element: SettingsTreeSettingElement): void {
1820
if (this.isVisible()
1821
&& this.settingsTree.hasElement(element)
1822
&& (!element.setting.deprecationMessage || element.isConfigured)) {
1823
this.settingsTree.rerender(element);
1824
}
1825
}
1826
1827
private refreshTree(): void {
1828
if (this.isVisible() && this.currentSettingsModel) {
1829
this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root));
1830
}
1831
}
1832
1833
private refreshTOCTree(): void {
1834
if (this.isVisible()) {
1835
this.tocTreeModel.update();
1836
this.tocTree.setChildren(null, createTOCIterator(this.tocTreeModel, this.tocTree));
1837
}
1838
}
1839
1840
private updateModifiedLabelForKey(key: string): void {
1841
if (!this.currentSettingsModel) {
1842
return;
1843
}
1844
// eslint-disable-next-line no-restricted-syntax
1845
const dataElements = this.currentSettingsModel.getElementsByName(key);
1846
const isModified = dataElements && dataElements[0] && dataElements[0].isConfigured; // all elements are either configured or not
1847
const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key);
1848
if (elements && elements[0]) {
1849
elements[0].classList.toggle('is-configured', !!isModified);
1850
}
1851
}
1852
1853
private async onSearchInputChanged(expandResults: boolean): Promise<void> {
1854
if (!this.currentSettingsModel) {
1855
// Initializing search widget value
1856
return;
1857
}
1858
1859
const query = this.searchWidget.getValue().trim();
1860
this.viewState.query = query;
1861
await this.triggerSearch(query.replace(/\u203A/g, ' '), expandResults);
1862
}
1863
1864
private parseSettingFromJSON(query: string): string | null {
1865
const match = query.match(/"([a-zA-Z.]+)": /);
1866
return match && match[1];
1867
}
1868
1869
/**
1870
* Toggles the visibility of the Settings editor table of contents during a search
1871
* depending on the behavior.
1872
*/
1873
private toggleTocBySearchBehaviorType() {
1874
const tocBehavior = this.configurationService.getValue<'filter' | 'hide'>(SEARCH_TOC_BEHAVIOR_KEY);
1875
const hideToc = tocBehavior === 'hide';
1876
if (hideToc) {
1877
this.splitView.setViewVisible(0, false);
1878
this.splitView.style({
1879
separatorBorder: Color.transparent
1880
});
1881
} else {
1882
this.layoutSplitView(this.dimension);
1883
}
1884
}
1885
1886
private async triggerSearch(query: string, expandResults: boolean): Promise<void> {
1887
const progressRunner = this.editorProgressService.show(true, 800);
1888
const showAdvanced = this.viewState.tagFilters?.has(ADVANCED_SETTING_TAG);
1889
this.viewState.tagFilters = new Set<string>();
1890
this.viewState.extensionFilters = new Set<string>();
1891
this.viewState.featureFilters = new Set<string>();
1892
this.viewState.idFilters = new Set<string>();
1893
this.viewState.languageFilter = undefined;
1894
if (query) {
1895
const parsedQuery = parseQuery(query);
1896
query = parsedQuery.query;
1897
parsedQuery.tags.forEach(tag => this.viewState.tagFilters!.add(tag));
1898
parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId));
1899
parsedQuery.featureFilters.forEach(feature => this.viewState.featureFilters!.add(feature));
1900
parsedQuery.idFilters.forEach(id => this.viewState.idFilters!.add(id));
1901
this.viewState.languageFilter = parsedQuery.languageFilter;
1902
}
1903
1904
if (showAdvanced !== this.viewState.tagFilters?.has(ADVANCED_SETTING_TAG)) {
1905
await this.onConfigUpdate();
1906
}
1907
1908
this.settingsTargetsWidget.updateLanguageFilterIndicators(this.viewState.languageFilter);
1909
1910
if (query && query !== '@') {
1911
query = this.parseSettingFromJSON(query) || query;
1912
await this.triggerFilterPreferences(query, expandResults, progressRunner);
1913
this.toggleTocBySearchBehaviorType();
1914
} else {
1915
if (this.viewState.tagFilters.size || this.viewState.extensionFilters.size || this.viewState.featureFilters.size || this.viewState.idFilters.size || this.viewState.languageFilter) {
1916
this.searchResultModel = this.createFilterModel();
1917
} else {
1918
this.searchResultModel = null;
1919
}
1920
1921
this.searchDelayer.cancel();
1922
if (this.searchInProgress) {
1923
this.searchInProgress.dispose(true);
1924
this.searchInProgress = null;
1925
}
1926
1927
if (expandResults) {
1928
this.tocTree.setFocus([]);
1929
this.viewState.categoryFilter = undefined;
1930
}
1931
this.tocTreeModel.currentSearchModel = this.searchResultModel;
1932
1933
if (this.searchResultModel) {
1934
// Added a filter model
1935
if (expandResults) {
1936
this.tocTree.setSelection([]);
1937
this.tocTree.expandAll();
1938
}
1939
this.refreshTOCTree();
1940
this.renderResultCountMessages(false);
1941
this.refreshTree();
1942
this.toggleTocBySearchBehaviorType();
1943
} else if (!this.tocTreeDisposed) {
1944
// Leaving search mode
1945
this.tocTree.collapseAll();
1946
this.refreshTOCTree();
1947
this.renderResultCountMessages(false);
1948
this.refreshTree();
1949
this.layoutSplitView(this.dimension);
1950
}
1951
progressRunner.done();
1952
}
1953
}
1954
1955
/**
1956
* Return a fake SearchResultModel which can hold a flat list of all settings, to be filtered (@modified etc)
1957
*/
1958
private createFilterModel(): SearchResultModel {
1959
const filterModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted());
1960
1961
const fullResult: ISearchResult = {
1962
filterMatches: [],
1963
exactMatch: false,
1964
};
1965
const shouldShowAdvanced = this.canShowAdvancedSettings();
1966
for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) {
1967
for (const sect of g.sections) {
1968
for (const setting of sect.settings) {
1969
if (!shouldShowAdvanced && !this.shouldShowSetting(setting)) {
1970
continue;
1971
}
1972
fullResult.filterMatches.push({
1973
setting,
1974
matches: [],
1975
matchType: SettingMatchType.None,
1976
keyMatchScore: 0,
1977
score: 0,
1978
providerName: FILTER_MODEL_SEARCH_PROVIDER_NAME
1979
});
1980
}
1981
}
1982
}
1983
1984
filterModel.setResult(0, fullResult);
1985
return filterModel;
1986
}
1987
1988
private async triggerFilterPreferences(query: string, expandResults: boolean, progressRunner: IProgressRunner): Promise<void> {
1989
if (this.searchInProgress) {
1990
this.searchInProgress.dispose(true);
1991
this.searchInProgress = null;
1992
}
1993
1994
const searchInProgress = this.searchInProgress = new CancellationTokenSource();
1995
return this.searchDelayer.trigger(async () => {
1996
if (searchInProgress.token.isCancellationRequested) {
1997
return;
1998
}
1999
this.disableAiSearchToggle();
2000
const localResults = await this.doLocalSearch(query, searchInProgress.token);
2001
if (!this.searchResultModel || searchInProgress.token.isCancellationRequested) {
2002
return;
2003
}
2004
this.searchResultModel.showAiResults = false;
2005
2006
if (localResults && localResults.filterMatches.length > 0) {
2007
// The remote results might take a while and
2008
// are always appended to the end anyway, so
2009
// show some results now.
2010
this.onDidFinishSearch(expandResults, undefined);
2011
}
2012
2013
if (!localResults || !localResults.exactMatch) {
2014
await this.doRemoteSearch(query, searchInProgress.token);
2015
}
2016
if (searchInProgress.token.isCancellationRequested) {
2017
return;
2018
}
2019
2020
if (this.aiSearchPromise) {
2021
this.aiSearchPromise.cancel();
2022
}
2023
2024
// Kick off an AI search in the background if the toggle is shown.
2025
// We purposely do not await it.
2026
if (this.searchInputActionBar && this.showAiResultsAction && this.searchInputActionBar.hasAction(this.showAiResultsAction)) {
2027
this.aiSearchPromise = createCancelablePromise(token => {
2028
return this.doAiSearch(query, token).then((results) => {
2029
if (results && this.showAiResultsAction) {
2030
this.showAiResultsAction.enabled = true;
2031
this.aiResultsAvailable.set(true);
2032
this.showAiResultsAction.label = SHOW_AI_RESULTS_ENABLED_LABEL;
2033
this.renderResultCountMessages(true);
2034
}
2035
}).catch(e => {
2036
if (!isCancellationError(e)) {
2037
this.logService.trace('Error during AI settings search:', e);
2038
}
2039
});
2040
});
2041
}
2042
2043
this.onDidFinishSearch(expandResults, progressRunner);
2044
});
2045
}
2046
2047
private onDidFinishSearch(expandResults: boolean, progressRunner: IProgressRunner | undefined): void {
2048
this.tocTreeModel.currentSearchModel = this.searchResultModel;
2049
if (expandResults) {
2050
this.tocTree.setFocus([]);
2051
this.viewState.categoryFilter = undefined;
2052
this.tocTree.expandAll();
2053
this.settingsTree.scrollTop = 0;
2054
}
2055
this.refreshTOCTree();
2056
this.renderTree(undefined, true);
2057
progressRunner?.done();
2058
}
2059
2060
private doLocalSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {
2061
const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
2062
return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, STRING_MATCH_SEARCH_PROVIDER_NAME, token);
2063
}
2064
2065
private doRemoteSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {
2066
const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
2067
if (!remoteSearchProvider) {
2068
return Promise.resolve(null);
2069
}
2070
return this.searchWithProvider(SearchResultIdx.Remote, remoteSearchProvider, TF_IDF_SEARCH_PROVIDER_NAME, token);
2071
}
2072
2073
private async doAiSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {
2074
const aiSearchProvider = this.preferencesSearchService.getAiSearchProvider(query);
2075
if (!aiSearchProvider) {
2076
return null;
2077
}
2078
2079
const embeddingsResults = await this.searchWithProvider(SearchResultIdx.Embeddings, aiSearchProvider, EMBEDDINGS_SEARCH_PROVIDER_NAME, token);
2080
if (!embeddingsResults || token.isCancellationRequested) {
2081
return null;
2082
}
2083
2084
const llmResults = await this.getLLMRankedResults(query, token);
2085
if (token.isCancellationRequested) {
2086
return null;
2087
}
2088
2089
return {
2090
filterMatches: embeddingsResults.filterMatches.concat(llmResults?.filterMatches ?? []),
2091
exactMatch: false
2092
};
2093
}
2094
2095
private async getLLMRankedResults(query: string, token: CancellationToken): Promise<ISearchResult | null> {
2096
const aiSearchProvider = this.preferencesSearchService.getAiSearchProvider(query);
2097
if (!aiSearchProvider) {
2098
return null;
2099
}
2100
2101
this.stopWatch.reset();
2102
const result = await aiSearchProvider.getLLMRankedResults(token);
2103
this.stopWatch.stop();
2104
2105
if (token.isCancellationRequested) {
2106
return null;
2107
}
2108
2109
// Only log the elapsed time if there are actual results.
2110
if (result && result.filterMatches.length > 0) {
2111
const elapsed = this.stopWatch.elapsed();
2112
this.logSearchPerformance(LLM_RANKED_SEARCH_PROVIDER_NAME, elapsed);
2113
}
2114
2115
this.searchResultModel!.setResult(SearchResultIdx.AiSelected, result);
2116
return result;
2117
}
2118
2119
private async searchWithProvider(type: SearchResultIdx, searchProvider: ISearchProvider, providerName: string, token: CancellationToken): Promise<ISearchResult | null> {
2120
this.stopWatch.reset();
2121
const result = await this._searchPreferencesModel(this.defaultSettingsEditorModel, searchProvider, token);
2122
this.stopWatch.stop();
2123
2124
if (token.isCancellationRequested) {
2125
// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
2126
return null;
2127
}
2128
2129
// Filter out advanced settings unless the advanced tag is explicitly set or setting matches an ID filter
2130
if (result && !this.canShowAdvancedSettings()) {
2131
result.filterMatches = result.filterMatches.filter(match => this.shouldShowSetting(match.setting));
2132
}
2133
2134
// Only log the elapsed time if there are actual results.
2135
if (result && result.filterMatches.length > 0) {
2136
const elapsed = this.stopWatch.elapsed();
2137
this.logSearchPerformance(providerName, elapsed);
2138
}
2139
2140
this.searchResultModel ??= this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted());
2141
this.searchResultModel.setResult(type, result);
2142
return result;
2143
}
2144
2145
private logSearchPerformance(providerName: string, elapsed: number): void {
2146
type SettingsEditorSearchPerformanceEvent = {
2147
providerName: string | undefined;
2148
elapsedMs: number;
2149
};
2150
type SettingsEditorSearchPerformanceClassification = {
2151
providerName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the search provider, if applicable.' };
2152
elapsedMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The time taken to perform the search, in milliseconds.' };
2153
owner: 'rzhao271';
2154
comment: 'Event emitted when the Settings editor calls a search provider to search for a setting';
2155
};
2156
this.telemetryService.publicLog2<SettingsEditorSearchPerformanceEvent, SettingsEditorSearchPerformanceClassification>('settingsEditor.searchPerformance', {
2157
providerName,
2158
elapsedMs: elapsed,
2159
});
2160
}
2161
2162
private renderResultCountMessages(showAiResultsMessage: boolean) {
2163
if (!this.currentSettingsModel) {
2164
return;
2165
}
2166
2167
this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 0
2168
? 'initial'
2169
: 'none';
2170
2171
if (!this.searchResultModel) {
2172
if (this.countElement.style.display !== 'none') {
2173
this.searchResultLabel = null;
2174
this.updateInputAriaLabel();
2175
this.countElement.style.display = 'none';
2176
this.countElement.innerText = '';
2177
this.layout(this.dimension);
2178
}
2179
2180
this.rootElement.classList.remove('no-results');
2181
this.splitView.el.style.visibility = 'visible';
2182
return;
2183
} else {
2184
const count = this.searchResultModel.getUniqueResultsCount();
2185
let resultString: string;
2186
2187
if (showAiResultsMessage) {
2188
switch (count) {
2189
case 0: resultString = localize('noResultsWithAiAvailable', "No Settings Found. AI Results Available"); break;
2190
case 1: resultString = localize('oneResultWithAiAvailable', "1 Setting Found. AI Results Available"); break;
2191
default: resultString = localize('moreThanOneResultWithAiAvailable', "{0} Settings Found. AI Results Available", count);
2192
}
2193
} else {
2194
switch (count) {
2195
case 0: resultString = localize('noResults', "No Settings Found"); break;
2196
case 1: resultString = localize('oneResult', "1 Setting Found"); break;
2197
default: resultString = localize('moreThanOneResult', "{0} Settings Found", count);
2198
}
2199
}
2200
2201
this.searchResultLabel = resultString;
2202
this.updateInputAriaLabel();
2203
this.countElement.innerText = resultString;
2204
aria.status(resultString);
2205
2206
if (this.countElement.style.display !== 'block') {
2207
this.countElement.style.display = 'block';
2208
}
2209
this.layout(this.dimension);
2210
this.rootElement.classList.toggle('no-results', count === 0);
2211
this.splitView.el.style.visibility = count === 0 ? 'hidden' : 'visible';
2212
}
2213
}
2214
2215
private async _searchPreferencesModel(model: ISettingsEditorModel, provider: ISearchProvider, token: CancellationToken): Promise<ISearchResult | null> {
2216
try {
2217
return await provider.searchModel(model, token);
2218
} catch (err) {
2219
if (isCancellationError(err)) {
2220
return Promise.reject(err);
2221
} else {
2222
return null;
2223
}
2224
}
2225
}
2226
2227
private layoutSplitView(dimension: DOM.Dimension): void {
2228
if (!this.isVisible()) {
2229
return;
2230
}
2231
const listHeight = dimension.height - (72 + 11 + 14 /* header height + editor padding */);
2232
2233
this.splitView.el.style.height = `${listHeight}px`;
2234
2235
// We call layout first so the splitView has an idea of how much
2236
// space it has, otherwise setViewVisible results in the first panel
2237
// showing up at the minimum size whenever the Settings editor
2238
// opens for the first time.
2239
this.splitView.layout(this.bodyContainer.clientWidth, listHeight);
2240
2241
const tocBehavior = this.configurationService.getValue<'filter' | 'hide'>(SEARCH_TOC_BEHAVIOR_KEY);
2242
const hideTocForSearch = tocBehavior === 'hide' && this.searchResultModel;
2243
if (!hideTocForSearch) {
2244
const firstViewWasVisible = this.splitView.isViewVisible(0);
2245
const firstViewVisible = this.bodyContainer.clientWidth >= SettingsEditor2.NARROW_TOTAL_WIDTH;
2246
2247
this.splitView.setViewVisible(0, firstViewVisible);
2248
// If the first view is again visible, and we have enough space, immediately set the
2249
// editor to use the reset width rather than the cached min width
2250
if (!firstViewWasVisible && firstViewVisible && this.bodyContainer.clientWidth >= SettingsEditor2.EDITOR_MIN_WIDTH + SettingsEditor2.TOC_RESET_WIDTH) {
2251
this.splitView.resizeView(0, SettingsEditor2.TOC_RESET_WIDTH);
2252
}
2253
this.splitView.style({
2254
separatorBorder: firstViewVisible ? this.theme.getColor(settingsSashBorder)! : Color.transparent
2255
});
2256
}
2257
}
2258
2259
protected override saveState(): void {
2260
if (this.isVisible()) {
2261
const searchQuery = this.searchWidget.getValue().trim();
2262
const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget;
2263
if (this.input) {
2264
this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target });
2265
}
2266
} else if (this.input) {
2267
this.editorMemento.clearEditorState(this.input, this.group);
2268
}
2269
2270
super.saveState();
2271
}
2272
}
2273
2274
class SyncControls extends Disposable {
2275
private readonly lastSyncedLabel!: HTMLElement;
2276
private readonly turnOnSyncButton!: Button;
2277
2278
private readonly _onDidChangeLastSyncedLabel = this._register(new Emitter<string>());
2279
public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event;
2280
2281
constructor(
2282
window: CodeWindow,
2283
container: HTMLElement,
2284
@ICommandService private readonly commandService: ICommandService,
2285
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
2286
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
2287
@ITelemetryService telemetryService: ITelemetryService,
2288
) {
2289
super();
2290
2291
const turnOnSyncButtonContainer = DOM.append(container, $('.turn-on-sync'));
2292
this.turnOnSyncButton = this._register(new Button(turnOnSyncButtonContainer, { title: true, ...defaultButtonStyles }));
2293
this.lastSyncedLabel = DOM.append(container, $('.last-synced-label'));
2294
DOM.hide(this.lastSyncedLabel);
2295
2296
this.turnOnSyncButton.enabled = true;
2297
this.turnOnSyncButton.label = localize('turnOnSyncButton', "Backup and Sync Settings");
2298
DOM.hide(this.turnOnSyncButton.element);
2299
2300
this._register(this.turnOnSyncButton.onDidClick(async () => {
2301
await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn');
2302
}));
2303
2304
this.updateLastSyncedTime();
2305
this._register(this.userDataSyncService.onDidChangeLastSyncTime(() => {
2306
this.updateLastSyncedTime();
2307
}));
2308
2309
const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer());
2310
updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window);
2311
2312
this.update();
2313
this._register(this.userDataSyncService.onDidChangeStatus(() => {
2314
this.update();
2315
}));
2316
2317
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => {
2318
this.update();
2319
}));
2320
}
2321
2322
private updateLastSyncedTime(): void {
2323
const last = this.userDataSyncService.lastSyncTime;
2324
let label: string;
2325
if (typeof last === 'number') {
2326
const d = fromNow(last, true, undefined, true);
2327
label = localize('lastSyncedLabel', "Last synced: {0}", d);
2328
} else {
2329
label = '';
2330
}
2331
2332
this.lastSyncedLabel.textContent = label;
2333
this._onDidChangeLastSyncedLabel.fire(label);
2334
}
2335
2336
private update(): void {
2337
if (this.userDataSyncService.status === SyncStatus.Uninitialized) {
2338
return;
2339
}
2340
2341
if (this.userDataSyncEnablementService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) {
2342
DOM.show(this.lastSyncedLabel);
2343
DOM.hide(this.turnOnSyncButton.element);
2344
} else {
2345
DOM.hide(this.lastSyncedLabel);
2346
DOM.show(this.turnOnSyncButton.element);
2347
}
2348
}
2349
}
2350
2351
interface ISettingsEditor2State {
2352
searchQuery: string;
2353
target: SettingsTarget;
2354
}
2355
2356