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