Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts
5263 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as DOM from '../../../../base/browser/dom.js';6import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';8import * as aria from '../../../../base/browser/ui/aria/aria.js';9import { Button } from '../../../../base/browser/ui/button/button.js';10import { Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js';11import { ToggleActionViewItem } from '../../../../base/browser/ui/toggle/toggle.js';12import { ITreeElement } from '../../../../base/browser/ui/tree/tree.js';13import { CodeWindow } from '../../../../base/browser/window.js';14import { Action } from '../../../../base/common/actions.js';15import { CancelablePromise, createCancelablePromise, Delayer, raceTimeout } from '../../../../base/common/async.js';16import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';17import { Color } from '../../../../base/common/color.js';18import { fromNow } from '../../../../base/common/date.js';19import { isCancellationError } from '../../../../base/common/errors.js';20import { Emitter, Event } from '../../../../base/common/event.js';21import { Iterable } from '../../../../base/common/iterator.js';22import { KeyCode } from '../../../../base/common/keyCodes.js';23import { Disposable, DisposableStore, dispose, type IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';24import * as platform from '../../../../base/common/platform.js';25import { StopWatch } from '../../../../base/common/stopwatch.js';26import { ThemeIcon } from '../../../../base/common/themables.js';27import { URI } from '../../../../base/common/uri.js';28import { ILanguageService } from '../../../../editor/common/languages/language.js';29import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';30import { localize } from '../../../../nls.js';31import { ICommandService } from '../../../../platform/commands/common/commands.js';32import { ConfigurationTarget, IConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js';33import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';34import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';35import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';36import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';37import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';38import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';39import { ILogService } from '../../../../platform/log/common/log.js';40import { IProductService } from '../../../../platform/product/common/productService.js';41import { IEditorProgressService, IProgressRunner } from '../../../../platform/progress/common/progress.js';42import { Registry } from '../../../../platform/registry/common/platform.js';43import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';44import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';45import { defaultButtonStyles, defaultToggleStyles } from '../../../../platform/theme/browser/defaultStyles.js';46import { asCssVariable, asCssVariableWithDefault, badgeBackground, badgeForeground, contrastBorder, editorForeground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js';47import { IThemeService } from '../../../../platform/theme/common/themeService.js';48import { IUserDataSyncEnablementService, IUserDataSyncService, SyncStatus } from '../../../../platform/userDataSync/common/userDataSync.js';49import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';50import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';51import { EditorPane } from '../../../browser/parts/editor/editorPane.js';52import { IEditorMemento, IEditorOpenContext, IEditorPane } from '../../../common/editor.js';53import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js';54import { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js';55import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';56import { IExtensionService } from '../../../services/extensions/common/extensions.js';57import { ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING, IOpenSettingsOptions, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsEditorOptions, ISettingsGroup, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from '../../../services/preferences/common/preferences.js';58import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js';59import { nullRange, Settings2EditorModel } from '../../../services/preferences/common/preferencesModels.js';60import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';61import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js';62import { SuggestEnabledInput } from '../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js';63import { ADVANCED_SETTING_TAG, CONTEXT_AI_SETTING_RESULTS_AVAILABLE, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, EMBEDDINGS_SEARCH_PROVIDER_NAME, ENABLE_LANGUAGE_FILTER, EXTENSION_FETCH_TIMEOUT_MS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, FILTER_MODEL_SEARCH_PROVIDER_NAME, getExperimentalExtensionToggleData, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, LLM_RANKED_SEARCH_PROVIDER_NAME, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_AI_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, SETTINGS_EDITOR_COMMAND_TOGGLE_AI_SEARCH, STRING_MATCH_SEARCH_PROVIDER_NAME, TF_IDF_SEARCH_PROVIDER_NAME, WorkbenchSettingsEditorSettings, WORKSPACE_TRUST_SETTING_TAG } from '../common/preferences.js';64import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js';65import './media/settingsEditor2.css';66import { preferencesAiResultsIcon, preferencesClearInputIcon, preferencesFilterIcon } from './preferencesIcons.js';67import { SettingsTarget, SettingsTargetsWidget } from './preferencesWidgets.js';68import { ISettingOverrideClickEvent } from './settingsEditorSettingIndicators.js';69import { getCommonlyUsedData, ITOCEntry, tocData } from './settingsLayout.js';70import { SettingsSearchFilterDropdownMenuActionViewItem } from './settingsSearchMenu.js';71import { AbstractSettingRenderer, createTocTreeForExtensionSettings, HeightChangeParams, ISettingLinkClickEvent, resolveConfiguredUntrustedSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from './settingsTree.js';72import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from './settingsTreeModels.js';73import { createTOCIterator, TOCTree, TOCTreeModel } from './tocTree.js';7475export const enum SettingsFocusContext {76Search,77TableOfContents,78SettingTree,79SettingControl80}8182export function createGroupIterator(group: SettingsTreeGroupElement): Iterable<ITreeElement<SettingsTreeGroupChild>> {83return Iterable.map(group.children, g => {84return {85element: g,86children: g instanceof SettingsTreeGroupElement ?87createGroupIterator(g) :88undefined89};90});91}9293const $ = DOM.$;9495interface IFocusEventFromScroll extends KeyboardEvent {96fromScroll: true;97}9899const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings");100const SEARCH_TOC_BEHAVIOR_KEY = 'workbench.settings.settingsSearchTocBehavior';101const SCROLL_BEHAVIOR_KEY = 'workbench.settings.scrollBehavior';102103const SHOW_AI_RESULTS_ENABLED_LABEL = localize('showAiResultsEnabled', "Show AI-recommended results");104const SHOW_AI_RESULTS_DISABLED_LABEL = localize('showAiResultsDisabled', "No AI results available at this time...");105106const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState';107108export class SettingsEditor2 extends EditorPane {109110static readonly ID: string = 'workbench.editor.settings2';111private static NUM_INSTANCES: number = 0;112private static SEARCH_DEBOUNCE: number = 200;113private static SETTING_UPDATE_FAST_DEBOUNCE: number = 200;114private static SETTING_UPDATE_SLOW_DEBOUNCE: number = 1000;115private static CONFIG_SCHEMA_UPDATE_DELAYER = 500;116private static TOC_MIN_WIDTH: number = 100;117private static TOC_RESET_WIDTH: number = 200;118private static EDITOR_MIN_WIDTH: number = 500;119// Below NARROW_TOTAL_WIDTH, we only render the editor rather than the ToC.120private static NARROW_TOTAL_WIDTH: number = this.TOC_RESET_WIDTH + this.EDITOR_MIN_WIDTH;121122private static SUGGESTIONS: string[] = [123`@${MODIFIED_SETTING_TAG}`,124'@tag:notebookLayout',125'@tag:notebookOutputLayout',126`@tag:${REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG}`,127`@tag:${WORKSPACE_TRUST_SETTING_TAG}`,128'@tag:sync',129'@tag:usesOnlineServices',130'@tag:telemetry',131'@tag:accessibility',132'@tag:preview',133'@tag:experimental',134`@tag:${ADVANCED_SETTING_TAG}`,135`@${ID_SETTING_TAG}`,136`@${EXTENSION_SETTING_TAG}`,137`@${FEATURE_SETTING_TAG}scm`,138`@${FEATURE_SETTING_TAG}explorer`,139`@${FEATURE_SETTING_TAG}search`,140`@${FEATURE_SETTING_TAG}debug`,141`@${FEATURE_SETTING_TAG}extensions`,142`@${FEATURE_SETTING_TAG}terminal`,143`@${FEATURE_SETTING_TAG}task`,144`@${FEATURE_SETTING_TAG}problems`,145`@${FEATURE_SETTING_TAG}output`,146`@${FEATURE_SETTING_TAG}comments`,147`@${FEATURE_SETTING_TAG}remote`,148`@${FEATURE_SETTING_TAG}timeline`,149`@${FEATURE_SETTING_TAG}notebook`,150`@${FEATURE_SETTING_TAG}chat`,151`@${POLICY_SETTING_TAG}`152];153154private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean {155if (Array.isArray(type)) {156// nullable integer/number or complex157return false;158}159return type === SettingValueType.Enum ||160type === SettingValueType.Array ||161type === SettingValueType.BooleanObject ||162type === SettingValueType.Object ||163type === SettingValueType.Complex ||164type === SettingValueType.Boolean ||165type === SettingValueType.Exclude ||166type === SettingValueType.Include;167}168169// (!) Lots of props that are set once on the first render170private defaultSettingsEditorModel!: Settings2EditorModel;171private readonly modelDisposables: DisposableStore;172173private rootElement!: HTMLElement;174private headerContainer!: HTMLElement;175private searchContainer: HTMLElement | null = null;176private bodyContainer!: HTMLElement;177private searchWidget!: SuggestEnabledInput;178private countElement!: HTMLElement;179private controlsElement!: HTMLElement;180private settingsTargetsWidget!: SettingsTargetsWidget;181182private splitView!: SplitView<number>;183184private settingsTreeContainer!: HTMLElement;185private settingsTree!: SettingsTree;186private settingRenderers!: SettingTreeRenderers;187private tocTreeModel!: TOCTreeModel;188private readonly settingsTreeModel = this._register(new MutableDisposable<SettingsTreeModel>());189private noResultsMessage!: HTMLElement;190private clearFilterLinkContainer!: HTMLElement;191192private tocTreeContainer!: HTMLElement;193private tocTree!: TOCTree;194195private searchDelayer: Delayer<void>;196private searchInProgress: CancellationTokenSource | null = null;197private aiSearchPromise: CancelablePromise<void> | null = null;198199private stopWatch: StopWatch;200201private showAiResultsAction: Action | null = null;202203private searchInputDelayer: Delayer<void>;204private updatedConfigSchemaDelayer: Delayer<void>;205206private settingFastUpdateDelayer: Delayer<void>;207private settingSlowUpdateDelayer: Delayer<void>;208private pendingSettingUpdate: { key: string; value: unknown; languageFilter: string | undefined } | null = null;209210private readonly viewState: ISettingsEditorViewState;211private readonly _searchResultModel = this._register(new MutableDisposable<SearchResultModel>());212private searchResultLabel: string | null = null;213private lastSyncedLabel: string | null = null;214private settingsOrderByTocIndex: Map<string, number> | null = null;215216private tocRowFocused: IContextKey<boolean>;217private settingRowFocused: IContextKey<boolean>;218private inSettingsEditorContextKey: IContextKey<boolean>;219private searchFocusContextKey: IContextKey<boolean>;220private aiResultsAvailable: IContextKey<boolean>;221222private scheduledRefreshes: Map<string, DisposableStore>;223private _currentFocusContext: SettingsFocusContext = SettingsFocusContext.Search;224225/** Don't spam warnings */226private hasWarnedMissingSettings = false;227private tocTreeDisposed = false;228229/** Persist the search query upon reloads */230private editorMemento: IEditorMemento<ISettingsEditor2State>;231232private tocFocusedElement: SettingsTreeGroupElement | null = null;233private treeFocusedElement: SettingsTreeElement | null = null;234private settingsTreeScrollTop = 0;235private dimension!: DOM.Dimension;236237private installedExtensionIds: string[] = [];238private dismissedExtensionSettings: string[] = [];239240private readonly DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY = 'settingsEditor2.dismissedExtensionSettings';241private readonly DISMISSED_EXTENSION_SETTINGS_DELIMITER = '\t';242243private readonly inputChangeListener: MutableDisposable<IDisposable>;244245private searchInputActionBar: ActionBar | null = null;246247constructor(248group: IEditorGroup,249@ITelemetryService telemetryService: ITelemetryService,250@IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService,251@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,252@IThemeService themeService: IThemeService,253@IPreferencesService private readonly preferencesService: IPreferencesService,254@IInstantiationService private readonly instantiationService: IInstantiationService,255@IPreferencesSearchService private readonly preferencesSearchService: IPreferencesSearchService,256@ILogService private readonly logService: ILogService,257@IContextKeyService contextKeyService: IContextKeyService,258@IStorageService private readonly storageService: IStorageService,259@IEditorGroupsService protected editorGroupService: IEditorGroupsService,260@IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService,261@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,262@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,263@IExtensionService private readonly extensionService: IExtensionService,264@ILanguageService private readonly languageService: ILanguageService,265@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,266@IProductService private readonly productService: IProductService,267@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,268@IEditorProgressService private readonly editorProgressService: IEditorProgressService,269@IUserDataProfileService userDataProfileService: IUserDataProfileService,270@IKeybindingService private readonly keybindingService: IKeybindingService,271@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService272) {273super(SettingsEditor2.ID, group, telemetryService, themeService, storageService);274this.searchDelayer = this._register(new Delayer(200));275this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL };276277this.settingFastUpdateDelayer = this._register(new Delayer<void>(SettingsEditor2.SETTING_UPDATE_FAST_DEBOUNCE));278this.settingSlowUpdateDelayer = this._register(new Delayer<void>(SettingsEditor2.SETTING_UPDATE_SLOW_DEBOUNCE));279280this.searchInputDelayer = this._register(new Delayer<void>(SettingsEditor2.SEARCH_DEBOUNCE));281this.updatedConfigSchemaDelayer = this._register(new Delayer<void>(SettingsEditor2.CONFIG_SCHEMA_UPDATE_DELAYER));282283this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);284this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);285this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService);286this.settingRowFocused = CONTEXT_SETTINGS_ROW_FOCUS.bindTo(contextKeyService);287this.aiResultsAvailable = CONTEXT_AI_SETTING_RESULTS_AVAILABLE.bindTo(contextKeyService);288289this.scheduledRefreshes = new Map<string, DisposableStore>();290this.stopWatch = new StopWatch(false);291292this.editorMemento = this.getEditorMemento<ISettingsEditor2State>(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY);293294this.dismissedExtensionSettings = this.storageService295.get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '')296.split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER);297298this._register(configurationService.onDidChangeConfiguration(e => {299if (e.affectedKeys.has(WorkbenchSettingsEditorSettings.ShowAISearchToggle)300|| e.affectedKeys.has(WorkbenchSettingsEditorSettings.EnableNaturalLanguageSearch)) {301this.updateAiSearchToggleVisibility();302}303if (e.affectsConfiguration(ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING)) {304this.onConfigUpdate(undefined, true, true);305}306if (e.source !== ConfigurationTarget.DEFAULT) {307this.onConfigUpdate(e.affectedKeys);308}309}));310311this._register(chatEntitlementService.onDidChangeSentiment(() => {312this.updateAiSearchToggleVisibility();313}));314315this._register(userDataProfileService.onDidChangeCurrentProfile(e => {316e.join(this.whenCurrentProfileChanged());317}));318319this._register(workspaceTrustManagementService.onDidChangeTrust(() => {320this.searchResultModel?.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());321322if (this.settingsTreeModel.value) {323this.settingsTreeModel.value.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());324this.renderTree();325}326}));327328this._register(configurationService.onDidChangeRestrictedSettings(e => {329if (e.default.length && this.currentSettingsModel) {330this.updateElementsByKey(new Set(e.default));331}332}));333334this._register(extensionManagementService.onDidInstallExtensions(() => {335this.refreshInstalledExtensionsList();336}));337this._register(extensionManagementService.onDidUninstallExtension(() => {338this.refreshInstalledExtensionsList();339}));340341this.modelDisposables = this._register(new DisposableStore());342343if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) {344SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`);345}346this.inputChangeListener = this._register(new MutableDisposable());347}348349private async whenCurrentProfileChanged(): Promise<void> {350this.updatedConfigSchemaDelayer.trigger(() => {351this.dismissedExtensionSettings = this.storageService352.get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '')353.split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER);354this.onConfigUpdate(undefined, true);355});356}357358private canShowAdvancedSettings(): boolean {359if (this.configurationService.getValue<boolean>(ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING) ?? false) {360return true;361}362return this.viewState.tagFilters?.has(ADVANCED_SETTING_TAG) ?? false;363}364365/**366* Determines whether a setting should be shown even when advanced settings are filtered out.367* Returns true if:368* - The setting is not tagged as advanced, OR369* - The setting matches an ID filter (@id:settingKey), OR370* - The setting key appears in the search query, OR371* - The @hasPolicy filter is active (policy settings should always be shown when filtering by policy)372*/373private shouldShowSetting(setting: ISetting): boolean {374if (!setting.tags?.includes(ADVANCED_SETTING_TAG)) {375return true;376}377if (this.viewState.idFilters?.has(setting.key)) {378return true;379}380if (this.viewState.query?.toLowerCase().includes(setting.key.toLowerCase())) {381return true;382}383if (this.viewState.tagFilters?.has(POLICY_SETTING_TAG)) {384return true;385}386return false;387}388389private disableAiSearchToggle(): void {390if (this.showAiResultsAction) {391this.showAiResultsAction.checked = false;392this.showAiResultsAction.enabled = false;393this.aiResultsAvailable.set(false);394this.showAiResultsAction.label = SHOW_AI_RESULTS_DISABLED_LABEL;395}396}397398private updateAiSearchToggleVisibility(): void {399if (!this.searchContainer || !this.showAiResultsAction || !this.searchInputActionBar) {400return;401}402403const showAiToggle = this.configurationService.getValue<boolean>(WorkbenchSettingsEditorSettings.ShowAISearchToggle);404const enableNaturalLanguageSearch = this.configurationService.getValue<boolean>(WorkbenchSettingsEditorSettings.EnableNaturalLanguageSearch);405const chatHidden = this.chatEntitlementService.sentiment.hidden || this.chatEntitlementService.sentiment.disabled;406const canShowToggle = showAiToggle && enableNaturalLanguageSearch && !chatHidden;407408const alreadyVisible = this.searchInputActionBar.hasAction(this.showAiResultsAction);409if (!alreadyVisible && canShowToggle) {410this.searchInputActionBar.push(this.showAiResultsAction, {411index: 0,412label: false,413icon: true414});415this.searchContainer.classList.add('with-ai-toggle');416} else if (alreadyVisible) {417this.searchInputActionBar.pull(0);418this.searchContainer.classList.remove('with-ai-toggle');419this.showAiResultsAction.checked = false;420}421}422423override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; }424override get maximumWidth(): number { return Number.POSITIVE_INFINITY; }425override get minimumHeight() { return 180; }426427// these setters need to exist because this extends from EditorPane428override set minimumWidth(value: number) { /*noop*/ }429override set maximumWidth(value: number) { /*noop*/ }430431private get currentSettingsModel(): SettingsTreeModel | undefined {432return this.searchResultModel || this.settingsTreeModel.value;433}434435private get searchResultModel(): SearchResultModel | null {436return this._searchResultModel.value ?? null;437}438439private set searchResultModel(value: SearchResultModel | null) {440this._searchResultModel.value = value ?? undefined;441442this.rootElement.classList.toggle('search-mode', !!this._searchResultModel.value);443}444445private get focusedSettingDOMElement(): HTMLElement | undefined {446const focused = this.settingsTree.getFocus()[0];447if (!(focused instanceof SettingsTreeSettingElement)) {448return;449}450451return this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), focused.setting.key)[0];452}453454get currentFocusContext() {455return this._currentFocusContext;456}457458protected createEditor(parent: HTMLElement): void {459parent.setAttribute('tabindex', '-1');460this.rootElement = DOM.append(parent, $('.settings-editor', { tabindex: '-1' }));461462this.createHeader(this.rootElement);463this.createBody(this.rootElement);464this.addCtrlAInterceptor(this.rootElement);465this.updateStyles();466467this._register(registerNavigableContainer({468name: 'settingsEditor2',469focusNotifiers: [this],470focusNextWidget: () => {471if (this.searchWidget.inputWidget.hasWidgetFocus()) {472this.focusTOC();473}474},475focusPreviousWidget: () => {476if (!this.searchWidget.inputWidget.hasWidgetFocus()) {477this.focusSearch();478}479}480}));481}482483override async setInput(input: SettingsEditor2Input, options: ISettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {484this.inSettingsEditorContextKey.set(true);485await super.setInput(input, options, context, token);486if (!this.input) {487return;488}489490const model = await this.input.resolve();491if (token.isCancellationRequested || !(model instanceof Settings2EditorModel)) {492return;493}494495this.modelDisposables.clear();496this.modelDisposables.add(model.onDidChangeGroups(() => {497this.updatedConfigSchemaDelayer.trigger(() => {498this.onConfigUpdate(undefined, false, true);499});500}));501this.defaultSettingsEditorModel = model;502503options = options || validateSettingsEditorOptions({});504if (!this.viewState.settingsTarget || !this.settingsTargetsWidget.settingsTarget) {505const optionsHasViewStateTarget = options.viewState && (options.viewState as ISettingsEditorViewState).settingsTarget;506if (!options.target && !optionsHasViewStateTarget) {507options.target = ConfigurationTarget.USER_LOCAL;508}509}510this._setOptions(options);511512// Don't block setInput on render (which can trigger an async search)513this.onConfigUpdate(undefined, true).then(() => {514// This event runs when the editor closes.515this.inputChangeListener.value = input.onWillDispose(() => {516this.searchWidget.setValue('');517});518519// Init TOC selection520this.updateTreeScrollSync();521});522523await this.refreshInstalledExtensionsList();524}525526private async refreshInstalledExtensionsList(): Promise<void> {527const installedExtensions = await this.extensionManagementService.getInstalled();528this.installedExtensionIds = installedExtensions529.filter(ext => ext.manifest.contributes?.configuration)530.map(ext => ext.identifier.id);531}532533private restoreCachedState(): ISettingsEditor2State | null {534const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input);535if (cachedState && typeof cachedState.target === 'object') {536cachedState.target = URI.revive(cachedState.target);537}538539if (cachedState) {540const settingsTarget = cachedState.target;541this.settingsTargetsWidget.settingsTarget = settingsTarget;542this.viewState.settingsTarget = settingsTarget;543if (!this.searchWidget.getValue()) {544this.searchWidget.setValue(cachedState.searchQuery);545}546}547548if (this.input) {549this.editorMemento.clearEditorState(this.input, this.group);550}551552return cachedState ?? null;553}554555override getViewState(): object | undefined {556return this.viewState;557}558559override setOptions(options: ISettingsEditorOptions | undefined): void {560super.setOptions(options);561562if (options) {563this._setOptions(options);564}565}566567private _setOptions(options: ISettingsEditorOptions): void {568if (options.focusSearch && !platform.isIOS) {569// isIOS - #122044570this.focusSearch();571}572573const recoveredViewState = options.viewState ?574options.viewState as ISettingsEditorViewState : undefined;575576const query: string | undefined = recoveredViewState?.query ?? options.query;577if (query !== undefined) {578this.searchWidget.setValue(query);579this.viewState.query = query;580}581582const target: SettingsTarget | undefined = options.folderUri ?? recoveredViewState?.settingsTarget ?? <SettingsTarget | undefined>options.target;583if (target) {584this.settingsTargetsWidget.updateTarget(target);585}586}587588override clearInput(): void {589this.inSettingsEditorContextKey.set(false);590super.clearInput();591}592593layout(dimension: DOM.Dimension): void {594this.dimension = dimension;595596if (!this.isVisible()) {597return;598}599600this.layoutSplitView(dimension);601602const innerWidth = Math.min(this.headerContainer.clientWidth, dimension.width) - 24 * 2; // 24px padding on left and right;603// minus padding inside inputbox, controls width, and extra padding before countElement604const monacoWidth = innerWidth - 10 - this.controlsElement.clientWidth - 12;605this.searchWidget.layout(new DOM.Dimension(monacoWidth, 20));606607this.rootElement.classList.toggle('narrow-width', dimension.width < SettingsEditor2.NARROW_TOTAL_WIDTH);608}609610override focus(): void {611super.focus();612613if (this._currentFocusContext === SettingsFocusContext.Search) {614if (!platform.isIOS) {615// #122044616this.focusSearch();617}618} else if (this._currentFocusContext === SettingsFocusContext.SettingControl) {619const element = this.focusedSettingDOMElement;620if (element) {621// eslint-disable-next-line no-restricted-syntax622const control = element.querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);623if (control) {624(<HTMLElement>control).focus();625return;626}627}628} else if (this._currentFocusContext === SettingsFocusContext.SettingTree) {629this.settingsTree.domFocus();630} else if (this._currentFocusContext === SettingsFocusContext.TableOfContents) {631this.tocTree.domFocus();632}633}634635protected override setEditorVisible(visible: boolean): void {636super.setEditorVisible(visible);637638if (!visible) {639// Wait for editor to be removed from DOM #106303640setTimeout(() => {641this.searchWidget.onHide();642this.settingRenderers.cancelSuggesters();643}, 0);644}645}646647focusSettings(focusSettingInput = false): void {648const focused = this.settingsTree.getFocus();649if (!focused.length) {650this.settingsTree.focusFirst();651}652653this.settingsTree.domFocus();654655if (focusSettingInput) {656// eslint-disable-next-line no-restricted-syntax657const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`);658if (controlInFocusedRow) {659(<HTMLElement>controlInFocusedRow).focus();660}661}662}663664focusTOC(): void {665this.tocTree.domFocus();666}667668showContextMenu(): void {669const focused = this.settingsTree.getFocus()[0];670const rowElement = this.focusedSettingDOMElement;671if (rowElement && focused instanceof SettingsTreeSettingElement) {672this.settingRenderers.showContextMenu(focused, rowElement);673}674}675676focusSearch(filter?: string, selectAll = true): void {677if (filter && this.searchWidget) {678this.searchWidget.setValue(filter);679}680681// Do not select all if the user is already searching.682this.searchWidget.focus(selectAll && !this.searchInputDelayer.isTriggered);683}684685clearSearchResults(): void {686this.disableAiSearchToggle();687this.searchWidget.setValue('');688this.focusSearch();689}690691clearSearchFilters(): void {692const query = this.searchWidget.getValue();693694const splitQuery = query.split(' ').filter(word => {695return word.length && !SettingsEditor2.SUGGESTIONS.some(suggestion => word.startsWith(suggestion));696});697698this.searchWidget.setValue(splitQuery.join(' '));699}700701private updateInputAriaLabel() {702let label = searchBoxLabel;703if (this.searchResultLabel) {704label += `. ${this.searchResultLabel}`;705}706707if (this.lastSyncedLabel) {708label += `. ${this.lastSyncedLabel}`;709}710711this.searchWidget.updateAriaLabel(label);712}713714/**715* Render the header of the Settings editor, which includes the content above the splitview.716*/717private createHeader(parent: HTMLElement): void {718this.headerContainer = DOM.append(parent, $('.settings-header'));719this.searchContainer = DOM.append(this.headerContainer, $('.search-container'));720721const clearInputAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS,722localize('clearInput', "Clear Settings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false,723async () => this.clearSearchResults()724));725726const showAiResultActionClassNames = ['action-label', ThemeIcon.asClassName(preferencesAiResultsIcon)];727this.showAiResultsAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_SHOW_AI_RESULTS,728SHOW_AI_RESULTS_DISABLED_LABEL, showAiResultActionClassNames.join(' '), true729));730this._register(this.showAiResultsAction.onDidChange(async () => {731await this.onDidToggleAiSearch();732}));733734const filterAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS,735localize('filterInput', "Filter Settings"), ThemeIcon.asClassName(preferencesFilterIcon)736));737738this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, this.searchContainer, {739triggerCharacters: ['@', ':'],740provideResults: (query: string) => {741// Based on testing, the trigger character is always at the end of the query.742// for the ':' trigger, only return suggestions if there was a '@' before it in the same word.743const queryParts = query.split(/\s/g);744if (queryParts[queryParts.length - 1].startsWith(`@${LANGUAGE_SETTING_TAG}`)) {745const sortedLanguages = this.languageService.getRegisteredLanguageIds().map(languageId => {746return `@${LANGUAGE_SETTING_TAG}${languageId} `;747}).sort();748return sortedLanguages.filter(langFilter => !query.includes(langFilter));749} else if (queryParts[queryParts.length - 1].startsWith(`@${EXTENSION_SETTING_TAG}`)) {750const installedExtensionsTags = this.installedExtensionIds.map(extensionId => {751return `@${EXTENSION_SETTING_TAG}${extensionId} `;752}).sort();753return installedExtensionsTags.filter(extFilter => !query.includes(extFilter));754} else if (query === '' || queryParts[queryParts.length - 1].startsWith('@')) {755return SettingsEditor2.SUGGESTIONS.filter(tag => !query.includes(tag)).map(tag => tag.endsWith(':') ? tag : tag + ' ');756}757return [];758}759}, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, {760placeholderText: searchBoxLabel,761focusContextKey: this.searchFocusContextKey,762styleOverrides: {763inputBorder: settingsTextInputBorder764}765// TODO: Aria-live766}));767this._register(this.searchWidget.onDidFocus(() => {768this._currentFocusContext = SettingsFocusContext.Search;769}));770this._register(this.searchWidget.onInputDidChange(() => {771const searchVal = this.searchWidget.getValue();772clearInputAction.enabled = !!searchVal;773this.searchInputDelayer.trigger(() => this.onSearchInputChanged(true));774}));775776const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));777headerControlsContainer.style.borderColor = asCssVariable(settingsHeaderBorder);778779const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));780this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true }));781this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL;782this._register(this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target)));783this._register(DOM.addDisposableListener(targetWidgetContainer, DOM.EventType.KEY_DOWN, e => {784const event = new StandardKeyboardEvent(e);785if (event.keyCode === KeyCode.DownArrow) {786this.focusSettings();787}788}));789790const headerRightControlsContainer = DOM.append(headerControlsContainer, $('.settings-right-controls'));791792const openSettingsJsonContainer = DOM.append(headerRightControlsContainer, $('.open-settings-json'));793const openSettingsJsonButton = this._register(new Button(openSettingsJsonContainer, { secondary: true, title: true, ...defaultButtonStyles }));794openSettingsJsonButton.label = localize('openSettingsJson', "Edit as JSON");795this._register(openSettingsJsonButton.onDidClick(() => this.openSettingsFile()));796797if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) {798const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerRightControlsContainer));799this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => {800this.lastSyncedLabel = lastSyncedLabel;801this.updateInputAriaLabel();802}));803}804805this.controlsElement = DOM.append(this.searchContainer, DOM.$('.search-container-widgets'));806807this.countElement = DOM.append(this.controlsElement, DOM.$('.settings-count-widget.monaco-count-badge.long'));808this.countElement.style.backgroundColor = asCssVariable(badgeBackground);809this.countElement.style.color = asCssVariable(badgeForeground);810this.countElement.style.border = `1px solid ${asCssVariableWithDefault(contrastBorder, asCssVariable(inputBackground))}`;811812this.searchInputActionBar = this._register(new ActionBar(this.controlsElement, {813actionViewItemProvider: (action, options) => {814if (action.id === filterAction.id) {815return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, options, this.actionRunner, this.searchWidget);816}817if (this.showAiResultsAction && action.id === this.showAiResultsAction.id) {818const keybindingLabel = this.keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_TOGGLE_AI_SEARCH)?.getLabel();819return new ToggleActionViewItem(null, action, { ...options, keybinding: keybindingLabel, toggleStyles: defaultToggleStyles });820}821return undefined;822}823}));824825const actionsToPush = [clearInputAction, filterAction];826this.searchInputActionBar.push(actionsToPush, { label: false, icon: true });827828this.disableAiSearchToggle();829this.updateAiSearchToggleVisibility();830}831832toggleAiSearch(): void {833if (this.searchInputActionBar && this.showAiResultsAction && this.searchInputActionBar.hasAction(this.showAiResultsAction)) {834if (!this.showAiResultsAction.enabled) {835aria.status(localize('noAiResults', "No AI results available at this time."));836}837this.showAiResultsAction.checked = !this.showAiResultsAction.checked;838}839}840841private async onDidToggleAiSearch(): Promise<void> {842if (this.searchResultModel && this.showAiResultsAction) {843this.searchResultModel.showAiResults = this.showAiResultsAction.checked ?? false;844this.renderResultCountMessages(false);845this.onDidFinishSearch(true, undefined);846}847}848849private onDidSettingsTargetChange(target: SettingsTarget): void {850this.viewState.settingsTarget = target;851852// TODO Instead of rebuilding the whole model, refresh and uncache the inspected setting value853this.onConfigUpdate(undefined, true);854}855856private onDidDismissExtensionSetting(extensionId: string): void {857if (!this.dismissedExtensionSettings.includes(extensionId)) {858this.dismissedExtensionSettings.push(extensionId);859}860this.storageService.store(861this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY,862this.dismissedExtensionSettings.join(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER),863StorageScope.PROFILE,864StorageTarget.USER865);866this.onConfigUpdate(undefined, true);867}868869private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void {870// eslint-disable-next-line no-restricted-syntax871const targetElement = this.currentSettingsModel?.getElementsByName(evt.targetKey)?.[0];872let revealFailed = false;873if (targetElement) {874let sourceTop = 0.5;875try {876const _sourceTop = this.settingsTree.getRelativeTop(evt.source);877if (_sourceTop !== null) {878sourceTop = _sourceTop;879}880} catch {881// e.g. clicked a searched element, now the search has been cleared882}883884// If we search for something and focus on a category, the settings tree885// only renders settings in that category.886// If the target display category is different than the source's, unfocus the category887// so that we can render all found settings again.888// Then, the reveal call will correctly find the target setting.889if (this.viewState.categoryFilter && evt.source.displayCategory !== targetElement.displayCategory) {890this.tocTree.setFocus([]);891}892try {893this.settingsTree.reveal(targetElement, sourceTop);894} catch (_) {895// The listwidget couldn't find the setting to reveal,896// even though it's in the model, meaning there might be a filter897// preventing it from showing up.898revealFailed = true;899}900901if (!revealFailed) {902// We need to shift focus from the setting that contains the link to the setting that's903// linked. Clicking on the link sets focus on the setting that contains the link,904// which is why we need the setTimeout.905setTimeout(() => {906this.settingsTree.setFocus([targetElement]);907}, 50);908909const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey);910if (domElements && domElements[0]) {911// eslint-disable-next-line no-restricted-syntax912const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);913if (control) {914(<HTMLElement>control).focus();915}916}917}918}919920if (!recursed && (!targetElement || revealFailed)) {921// We'll call this event handler again after clearing the search query,922// so that more settings show up in the list.923const p = this.triggerSearch('', true);924p.then(() => {925this.searchWidget.setValue('');926this.onDidClickSetting(evt, true);927});928}929}930931switchToSettingsFile(): Promise<IEditorPane | undefined> {932const query = parseQuery(this.searchWidget.getValue()).query;933return this.openSettingsFile({ query });934}935936private async openSettingsFile(options?: ISettingsEditorOptions): Promise<IEditorPane | undefined> {937const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget;938939const openOptions: IOpenSettingsOptions = { jsonEditor: true, groupId: this.group.id, ...options };940if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) {941if (options?.revealSetting) {942const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();943const configurationScope = configurationProperties[options?.revealSetting.key]?.scope;944if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) {945return this.preferencesService.openApplicationSettings(openOptions);946}947}948return this.preferencesService.openUserSettings(openOptions);949} else if (currentSettingsTarget === ConfigurationTarget.USER_REMOTE) {950return this.preferencesService.openRemoteSettings(openOptions);951} else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) {952return this.preferencesService.openWorkspaceSettings(openOptions);953} else if (URI.isUri(currentSettingsTarget)) {954return this.preferencesService.openFolderSettings({ folderUri: currentSettingsTarget, ...openOptions });955}956957return undefined;958}959960private createBody(parent: HTMLElement): void {961this.bodyContainer = DOM.append(parent, $('.settings-body'));962963this.noResultsMessage = DOM.append(this.bodyContainer, $('.no-results-message'));964965this.noResultsMessage.innerText = localize('noResults', "No Settings Found");966967this.clearFilterLinkContainer = $('span.clear-search-filters');968969this.clearFilterLinkContainer.textContent = ' - ';970const clearFilterLink = DOM.append(this.clearFilterLinkContainer, $('a.pointer.prominent', { tabindex: 0 }, localize('clearSearchFilters', 'Clear Filters')));971this._register(DOM.addDisposableListener(clearFilterLink, DOM.EventType.CLICK, (e: MouseEvent) => {972DOM.EventHelper.stop(e, false);973this.clearSearchFilters();974}));975976DOM.append(this.noResultsMessage, this.clearFilterLinkContainer);977978this.noResultsMessage.style.color = asCssVariable(editorForeground);979980this.tocTreeContainer = $('.settings-toc-container');981this.settingsTreeContainer = $('.settings-tree-container');982983this.createTOC(this.tocTreeContainer);984this.createSettingsTree(this.settingsTreeContainer);985986this.splitView = this._register(new SplitView(this.bodyContainer, {987orientation: Orientation.HORIZONTAL,988proportionalLayout: true989}));990const startingWidth = this.storageService.getNumber('settingsEditor2.splitViewWidth', StorageScope.PROFILE, SettingsEditor2.TOC_RESET_WIDTH);991this.splitView.addView({992onDidChange: Event.None,993element: this.tocTreeContainer,994minimumSize: SettingsEditor2.TOC_MIN_WIDTH,995maximumSize: Number.POSITIVE_INFINITY,996layout: (width, _, height) => {997this.tocTreeContainer.style.width = `${width}px`;998this.tocTree.layout(height, width);999}1000}, startingWidth, undefined, true);1001this.splitView.addView({1002onDidChange: Event.None,1003element: this.settingsTreeContainer,1004minimumSize: SettingsEditor2.EDITOR_MIN_WIDTH,1005maximumSize: Number.POSITIVE_INFINITY,1006layout: (width, _, height) => {1007this.settingsTreeContainer.style.width = `${width}px`;1008this.settingsTree.layout(height, width);1009}1010}, Sizing.Distribute, undefined, true);1011this._register(this.splitView.onDidSashReset(() => {1012const totalSize = this.splitView.getViewSize(0) + this.splitView.getViewSize(1);1013this.splitView.resizeView(0, SettingsEditor2.TOC_RESET_WIDTH);1014this.splitView.resizeView(1, totalSize - SettingsEditor2.TOC_RESET_WIDTH);1015}));1016this._register(this.splitView.onDidSashChange(() => {1017const width = this.splitView.getViewSize(0);1018this.storageService.store('settingsEditor2.splitViewWidth', width, StorageScope.PROFILE, StorageTarget.USER);1019}));1020const borderColor = this.theme.getColor(settingsSashBorder)!;1021this.splitView.style({ separatorBorder: borderColor });1022}10231024private addCtrlAInterceptor(container: HTMLElement): void {1025this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {1026if (1027e.keyCode === KeyCode.KeyA &&1028(platform.isMacintosh ? e.metaKey : e.ctrlKey) &&1029!DOM.isEditableElement(e.target)1030) {1031// Avoid browser ctrl+a1032e.browserEvent.stopPropagation();1033e.browserEvent.preventDefault();1034}1035}));1036}10371038private createTOC(container: HTMLElement): void {1039this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState);10401041this.tocTree = this._register(this.instantiationService.createInstance(TOCTree,1042DOM.append(container, $('.settings-toc-wrapper', {1043'role': 'navigation',1044'aria-label': localize('settings', "Settings"),1045})),1046this.viewState));1047this.tocTreeDisposed = false;10481049this._register(this.tocTree.onDidFocus(() => {1050this._currentFocusContext = SettingsFocusContext.TableOfContents;1051}));10521053this._register(this.tocTree.onDidChangeFocus(e => {1054const element: SettingsTreeGroupElement | null = e.elements?.[0] ?? null;1055if (this.tocFocusedElement === element) {1056return;1057}10581059this.tocFocusedElement = element;1060this.tocTree.setSelection(element ? [element] : []);1061const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY);1062if (this.searchResultModel || scrollBehavior === 'paginated') {1063// In search mode or paginated mode, filter to show only the selected category1064if (this.viewState.categoryFilter !== element) {1065this.viewState.categoryFilter = element ?? undefined;1066// Force render in this case, because1067// onDidClickSetting relies on the updated view.1068this.renderTree(undefined, true);1069this.settingsTree.scrollTop = 0;1070}1071} else {1072// In continuous mode, clear any category filter that may have been set in paginated mode1073if (this.viewState.categoryFilter) {1074this.viewState.categoryFilter = undefined;1075this.renderTree(undefined, true);1076}1077if (element && (!e.browserEvent || !(<IFocusEventFromScroll>e.browserEvent).fromScroll)) {1078let targetElement = element;1079// Searches equivalent old Object currently living in the Tree nodes.1080if (!this.settingsTree.hasElement(targetElement)) {1081if (element instanceof SettingsTreeGroupElement) {1082const targetId = element.id;10831084const findInViewNodes = (nodes: any[]): SettingsTreeGroupElement | undefined => {1085for (const node of nodes) {1086if (node.element instanceof SettingsTreeGroupElement && node.element.id === targetId) {1087return node.element;1088}1089if (node.children && node.children.length > 0) {1090const found = findInViewNodes(node.children);1091if (found) {1092return found;1093}1094}1095}1096return undefined;1097};10981099try {1100const rootNode = this.settingsTree.getNode(null);1101if (rootNode && rootNode.children) {1102const foundOldElement = findInViewNodes(rootNode.children);1103if (foundOldElement) {1104// Now we don't reveal the New Object, reveal the Old Object"1105targetElement = foundOldElement;1106}1107}1108} catch (err) {1109// Tree might be in an invalid state, ignore1110}1111}1112}11131114if (this.settingsTree.hasElement(targetElement)) {1115this.settingsTree.reveal(targetElement, 0);1116this.settingsTree.setFocus([targetElement]);1117}1118}1119}1120}));11211122this._register(this.tocTree.onDidFocus(() => {1123this.tocRowFocused.set(true);1124}));11251126this._register(this.tocTree.onDidBlur(() => {1127this.tocRowFocused.set(false);1128}));11291130this._register(this.tocTree.onDidDispose(() => {1131this.tocTreeDisposed = true;1132}));1133}11341135private applyFilter(filter: string) {1136if (this.searchWidget && !this.searchWidget.getValue().includes(filter)) {1137// Prepend the filter to the query.1138const newQuery = `${filter} ${this.searchWidget.getValue().trimStart()}`;1139this.focusSearch(newQuery, false);1140}1141}11421143private removeLanguageFilters() {1144if (this.searchWidget && this.searchWidget.getValue().includes(`@${LANGUAGE_SETTING_TAG}`)) {1145const query = this.searchWidget.getValue().split(' ');1146const newQuery = query.filter(word => !word.startsWith(`@${LANGUAGE_SETTING_TAG}`)).join(' ');1147this.focusSearch(newQuery, false);1148}1149}11501151private createSettingsTree(container: HTMLElement): void {1152this.settingRenderers = this._register(this.instantiationService.createInstance(SettingTreeRenderers));1153this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope)));1154this._register(this.settingRenderers.onDidDismissExtensionSetting((e) => this.onDidDismissExtensionSetting(e)));1155this._register(this.settingRenderers.onDidOpenSettings(settingKey => {1156this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } });1157}));1158this._register(this.settingRenderers.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName)));1159this._register(this.settingRenderers.onDidFocusSetting(element => {1160this.settingsTree.setFocus([element]);1161this._currentFocusContext = SettingsFocusContext.SettingControl;1162this.settingRowFocused.set(false);1163}));1164this._register(this.settingRenderers.onDidChangeSettingHeight((params: HeightChangeParams) => {1165const { element, height } = params;1166try {1167this.settingsTree.updateElementHeight(element, height);1168} catch (e) {1169// the element was not found1170}1171}));1172this._register(this.settingRenderers.onApplyFilter((filter) => this.applyFilter(filter)));1173this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => {1174this.removeLanguageFilters();1175if (element.language) {1176this.applyFilter(`@${LANGUAGE_SETTING_TAG}${element.language}`);1177}11781179if (element.scope === 'workspace') {1180this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE);1181} else if (element.scope === 'user') {1182this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL);1183} else if (element.scope === 'remote') {1184this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE);1185}1186this.applyFilter(`@${ID_SETTING_TAG}${element.settingKey}`);1187}));11881189this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree,1190container,1191this.viewState,1192this.settingRenderers.allRenderers));11931194this._register(this.settingsTree.onDidScroll(() => {1195if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) {1196return;1197}11981199this.settingsTreeScrollTop = this.settingsTree.scrollTop;12001201// setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when1202// setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync1203setTimeout(() => {1204this.updateTreeScrollSync();1205}, 0);1206}));12071208this._register(this.settingsTree.onDidFocus(() => {1209const classList = container.ownerDocument.activeElement?.classList;1210if (classList && classList.contains('monaco-list') && classList.contains('settings-editor-tree')) {1211this._currentFocusContext = SettingsFocusContext.SettingTree;1212this.settingRowFocused.set(true);1213this.treeFocusedElement ??= this.settingsTree.firstVisibleElement ?? null;1214if (this.treeFocusedElement) {1215this.treeFocusedElement.tabbable = true;1216}1217}1218}));12191220this._register(this.settingsTree.onDidBlur(() => {1221this.settingRowFocused.set(false);1222// Clear out the focused element, otherwise it could be1223// out of date during the next onDidFocus event.1224this.treeFocusedElement = null;1225}));12261227// There is no different select state in the settings tree1228this._register(this.settingsTree.onDidChangeFocus(e => {1229const element = e.elements[0];1230if (this.treeFocusedElement === element) {1231return;1232}12331234if (this.treeFocusedElement) {1235this.treeFocusedElement.tabbable = false;1236}12371238this.treeFocusedElement = element;12391240if (this.treeFocusedElement) {1241this.treeFocusedElement.tabbable = true;1242}12431244this.settingsTree.setSelection(element ? [element] : []);1245}));1246}12471248private onDidChangeSetting(key: string, value: unknown, type: SettingValueType | SettingValueType[], manualReset: boolean, scope: ConfigurationScope | undefined): void {1249const parsedQuery = parseQuery(this.searchWidget.getValue());1250const languageFilter = parsedQuery.languageFilter;1251if (manualReset || (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key)) {1252this.updateChangedSetting(key, value, manualReset, languageFilter, scope);1253}12541255this.pendingSettingUpdate = { key, value, languageFilter };1256if (SettingsEditor2.shouldSettingUpdateFast(type)) {1257this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope));1258} else {1259this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope));1260}1261}12621263private updateTreeScrollSync(): void {1264this.settingRenderers.cancelSuggesters();1265if (this.searchResultModel) {1266return;1267}12681269// In paginated mode, we don't sync scroll position since categories are filtered1270const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY);1271if (scrollBehavior === 'paginated') {1272return;1273}12741275if (!this.tocTreeModel) {1276return;1277}12781279const elementToSync = this.settingsTree.firstVisibleElement;1280const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent :1281elementToSync instanceof SettingsTreeGroupElement ? elementToSync :1282null;12831284// 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 because1285// it is focused. So, bail if element doesn't exist in the TOC.1286let nodeExists = true;1287try { this.tocTree.getNode(element); } catch (e) { nodeExists = false; }1288if (!nodeExists) {1289return;1290}12911292if (element && this.tocTree.getSelection()[0] !== element) {1293const ancestors = this.getAncestors(element);1294ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));12951296this.tocTree.reveal(element);1297const elementTop = this.tocTree.getRelativeTop(element);1298if (typeof elementTop !== 'number') {1299return;1300}13011302this.tocTree.collapseAll();13031304ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));1305if (elementTop < 0 || elementTop > 1) {1306this.tocTree.reveal(element);1307} else {1308this.tocTree.reveal(element, elementTop);1309}13101311this.tocTree.expand(element);13121313this.tocTree.setSelection([element]);13141315const fakeKeyboardEvent = new KeyboardEvent('keydown');1316(<IFocusEventFromScroll>fakeKeyboardEvent).fromScroll = true;1317this.tocTree.setFocus([element], fakeKeyboardEvent);1318}1319}13201321private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] {1322const ancestors: SettingsTreeElement[] = [];13231324while (element.parent) {1325if (element.parent.id !== 'root') {1326ancestors.push(element.parent);1327}13281329element = element.parent;1330}13311332return ancestors.reverse();1333}13341335private updateChangedSetting(key: string, value: unknown, manualReset: boolean, languageFilter: string | undefined, scope: ConfigurationScope | undefined): Promise<void> {1336// ConfigurationService displays the error if this fails.1337// Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change.1338const settingsTarget = this.settingsTargetsWidget.settingsTarget;1339const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined;1340const configurationTarget = <ConfigurationTarget | null>(resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget) ?? ConfigurationTarget.USER_LOCAL;1341const overrides: IConfigurationUpdateOverrides = { resource, overrideIdentifiers: languageFilter ? [languageFilter] : undefined };13421343const configurationTargetIsWorkspace = configurationTarget === ConfigurationTarget.WORKSPACE || configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER;13441345const userPassedInManualReset = configurationTargetIsWorkspace || !!languageFilter;1346const isManualReset = userPassedInManualReset ? manualReset : value === undefined;13471348// If the user is changing the value back to the default, and we're not targeting a workspace scope, do a 'reset' instead1349const inspected = this.configurationService.inspect(key, overrides);1350if (!userPassedInManualReset && inspected.defaultValue === value) {1351value = undefined;1352}13531354return this.configurationService.updateValue(key, value, overrides, configurationTarget, { handleDirtyFile: 'save' })1355.then(() => {1356const query = this.searchWidget.getValue();1357if (query.includes(`@${MODIFIED_SETTING_TAG}`)) {1358// The user might have reset a setting.1359this.refreshTOCTree();1360}1361this.renderTree(key, isManualReset);1362this.pendingSettingUpdate = null;13631364const reportModifiedProps = {1365key,1366query,1367searchResults: this.searchResultModel?.getUniqueSearchResults() ?? null,1368rawResults: this.searchResultModel?.getRawResults() ?? null,1369showConfiguredOnly: !!this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG),1370isReset: typeof value === 'undefined',1371settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget1372};1373return this.reportModifiedSetting(reportModifiedProps);1374});1375}13761377private reportModifiedSetting(props: { key: string; query: string; searchResults: ISearchResult | null; rawResults: ISearchResult[] | null; showConfiguredOnly: boolean; isReset: boolean; settingsTarget: SettingsTarget }): void {1378type SettingsEditorModifiedSettingEvent = {1379key: string;1380groupId: string | undefined;1381providerName: string | undefined;1382nlpIndex: number | undefined;1383displayIndex: number | undefined;1384showConfiguredOnly: boolean;1385isReset: boolean;1386target: string;1387};1388type SettingsEditorModifiedSettingClassification = {1389key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The setting that is being modified.' };1390groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' };1391providerName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the search provider, if applicable.' };1392nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The index of the setting in the remote search provider results, if applicable.' };1393displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The index of the setting in the combined search results, if applicable.' };1394showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is in the modified view, which shows configured settings only.' };1395isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Identifies whether a setting was reset to its default value.' };1396target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The scope of the setting, such as user or workspace.' };1397owner: 'rzhao271';1398comment: 'Event emitted when the user modifies a setting in the Settings editor';1399};14001401let groupId: string | undefined = undefined;1402let providerName: string | undefined = undefined;1403let nlpIndex: number | undefined = undefined;1404let displayIndex: number | undefined = undefined;1405if (props.searchResults) {1406displayIndex = props.searchResults.filterMatches.findIndex(m => m.setting.key === props.key);14071408if (this.searchResultModel) {1409providerName = props.searchResults.filterMatches.find(m => m.setting.key === props.key)?.providerName;1410const rawResults = this.searchResultModel.getRawResults();1411if (rawResults[SearchResultIdx.Local] && displayIndex >= 0) {1412const settingInLocalResults = rawResults[SearchResultIdx.Local].filterMatches.some(m => m.setting.key === props.key);1413groupId = settingInLocalResults ? 'local' : 'remote';1414}1415if (rawResults[SearchResultIdx.Remote]) {1416const _nlpIndex = rawResults[SearchResultIdx.Remote].filterMatches.findIndex(m => m.setting.key === props.key);1417nlpIndex = _nlpIndex >= 0 ? _nlpIndex : undefined;1418}1419}1420}14211422const reportedTarget = props.settingsTarget === ConfigurationTarget.USER_LOCAL ? 'user' :1423props.settingsTarget === ConfigurationTarget.USER_REMOTE ? 'user_remote' :1424props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' :1425'folder';14261427const data = {1428key: props.key,1429groupId,1430providerName,1431nlpIndex,1432displayIndex,1433showConfiguredOnly: props.showConfiguredOnly,1434isReset: props.isReset,1435target: reportedTarget1436};14371438this.telemetryService.publicLog2<SettingsEditorModifiedSettingEvent, SettingsEditorModifiedSettingClassification>('settingsEditor.settingModified', data);1439}14401441private scheduleRefresh(element: HTMLElement, key = ''): void {1442if (key && this.scheduledRefreshes.has(key)) {1443return;1444}14451446if (!key) {1447dispose(this.scheduledRefreshes.values());1448this.scheduledRefreshes.clear();1449}14501451const store = new DisposableStore();1452const scheduledRefreshTracker = DOM.trackFocus(element);1453store.add(scheduledRefreshTracker);1454store.add(scheduledRefreshTracker.onDidBlur(() => {1455this.scheduledRefreshes.get(key)?.dispose();1456this.scheduledRefreshes.delete(key);1457this.onConfigUpdate(new Set([key]));1458}));1459this.scheduledRefreshes.set(key, store);1460}14611462private createSettingsOrderByTocIndex(resolvedSettingsRoot: ITOCEntry<ISetting>): Map<string, number> {1463const index = new Map<string, number>();1464function indexSettings(resolvedSettingsRoot: ITOCEntry<ISetting>, counter = 0): number {1465if (resolvedSettingsRoot.settings) {1466for (const setting of resolvedSettingsRoot.settings) {1467if (!index.has(setting.key)) {1468index.set(setting.key, counter++);1469}1470}1471}1472if (resolvedSettingsRoot.children) {1473for (const child of resolvedSettingsRoot.children) {1474counter = indexSettings(child, counter);1475}1476}1477return counter;1478}1479indexSettings(resolvedSettingsRoot);1480return index;1481}14821483private refreshModels(resolvedSettingsRoot: ITOCEntry<ISetting>) {1484// Both calls to refreshModels require a valid settingsTreeModel.1485this.settingsTreeModel.value!.update(resolvedSettingsRoot);1486this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.value!.root;1487this.settingsOrderByTocIndex = this.createSettingsOrderByTocIndex(resolvedSettingsRoot);1488}14891490private async onConfigUpdate(keys?: ReadonlySet<string>, forceRefresh = false, triggerSearch = false): Promise<void> {1491if (keys && this.settingsTreeModel) {1492return this.updateElementsByKey(keys);1493}14941495if (!this.defaultSettingsEditorModel) {1496return;1497}14981499const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed1500const coreSettingsGroups = [], extensionSettingsGroups = [];1501for (const group of groups) {1502if (group.extensionInfo) {1503extensionSettingsGroups.push(group);1504} else {1505coreSettingsGroups.push(group);1506}1507}1508const filter = this.canShowAdvancedSettings() ? undefined : { exclude: { tags: [ADVANCED_SETTING_TAG] } };15091510const settingsResult = resolveSettingsTree(tocData, coreSettingsGroups, filter, this.logService);1511const resolvedSettingsRoot = settingsResult.tree;15121513// Warn for settings not included in layout1514if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) {1515const settingKeyList: string[] = [];1516settingsResult.leftoverSettings.forEach(s => {1517settingKeyList.push(s.key);1518});15191520this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`);1521this.hasWarnedMissingSettings = true;1522}15231524const additionalGroups: ISettingsGroup[] = [];1525let setAdditionalGroups = false;1526const toggleData = await getExperimentalExtensionToggleData(this.chatEntitlementService, this.extensionGalleryService, this.productService);1527if (toggleData && groups.filter(g => g.extensionInfo).length) {1528for (const key in toggleData.settingsEditorRecommendedExtensions) {1529const extension: IGalleryExtension = toggleData.recommendedExtensionsGalleryInfo[key];1530if (!extension) {1531continue;1532}15331534const extensionId = extension.identifier.id;1535// prevent race between extension update handler and this (onConfigUpdate) handler1536await this.refreshInstalledExtensionsList();1537const extensionInstalled = this.installedExtensionIds.includes(extensionId);15381539// Drill down to see whether the group and setting already exist1540// and need to be removed.1541const matchingGroupIndex = groups.findIndex(g =>1542g.extensionInfo && g.extensionInfo!.id.toLowerCase() === extensionId.toLowerCase() &&1543g.sections.length === 1 && g.sections[0].settings.length === 1 && g.sections[0].settings[0].displayExtensionId1544);1545if (extensionInstalled || this.dismissedExtensionSettings.includes(extensionId)) {1546if (matchingGroupIndex !== -1) {1547groups.splice(matchingGroupIndex, 1);1548setAdditionalGroups = true;1549}1550continue;1551}15521553if (matchingGroupIndex !== -1) {1554continue;1555}15561557// Create the entry. extensionInstalled is false in this case.1558let manifest: IExtensionManifest | null = null;1559try {1560manifest = await raceTimeout(1561this.extensionGalleryService.getManifest(extension, CancellationToken.None),1562EXTENSION_FETCH_TIMEOUT_MS1563) ?? null;1564} catch (e) {1565// Likely a networking issue.1566// Skip adding a button for this extension to the Settings editor.1567continue;1568}15691570if (manifest === null) {1571continue;1572}15731574const contributesConfiguration = manifest?.contributes?.configuration;15751576let groupTitle: string | undefined;1577if (!Array.isArray(contributesConfiguration)) {1578groupTitle = contributesConfiguration?.title;1579} else if (contributesConfiguration.length === 1) {1580groupTitle = contributesConfiguration[0].title;1581}15821583const recommendationInfo = toggleData.settingsEditorRecommendedExtensions[key];1584const extensionName = extension.displayName ?? extension.name ?? extensionId;1585const settingKey = `${key}.manageExtension`;1586const setting: ISetting = {1587range: nullRange,1588key: settingKey,1589keyRange: nullRange,1590value: null,1591valueRange: nullRange,1592description: [recommendationInfo.onSettingsEditorOpen?.descriptionOverride ?? extension.description],1593descriptionIsMarkdown: false,1594descriptionRanges: [],1595scope: ConfigurationScope.WINDOW,1596type: 'null',1597displayExtensionId: extensionId,1598extensionGroupTitle: groupTitle ?? extensionName,1599categoryLabel: 'Extensions',1600title: extensionName1601};1602const additionalGroup: ISettingsGroup = {1603sections: [{1604settings: [setting],1605}],1606id: extensionId,1607title: setting.extensionGroupTitle!,1608titleRange: nullRange,1609range: nullRange,1610extensionInfo: {1611id: extensionId,1612displayName: extension.displayName,1613}1614};1615groups.push(additionalGroup);1616additionalGroups.push(additionalGroup);1617setAdditionalGroups = true;1618}1619}16201621resolvedSettingsRoot.children!.push(await createTocTreeForExtensionSettings(this.extensionService, extensionSettingsGroups, filter));16221623resolvedSettingsRoot.children!.unshift(getCommonlyUsedData(groups));16241625if (toggleData && setAdditionalGroups) {1626// Add the additional groups to the model to help with searching.1627this.defaultSettingsEditorModel.setAdditionalGroups(additionalGroups);1628}16291630if (!this.workspaceTrustManagementService.isWorkspaceTrusted() && (this.viewState.settingsTarget instanceof URI || this.viewState.settingsTarget === ConfigurationTarget.WORKSPACE)) {1631const configuredUntrustedWorkspaceSettings = resolveConfiguredUntrustedSettings(groups, this.viewState.settingsTarget, this.viewState.languageFilter, this.configurationService);1632if (configuredUntrustedWorkspaceSettings.length) {1633resolvedSettingsRoot.children!.unshift({1634id: 'workspaceTrust',1635label: localize('settings require trust', "Workspace Trust"),1636settings: configuredUntrustedWorkspaceSettings1637});1638}1639}16401641this.searchResultModel?.updateChildren();16421643const firstVisibleElement = this.settingsTree.firstVisibleElement;1644let anchorId: string | undefined;16451646if (firstVisibleElement instanceof SettingsTreeSettingElement) {1647anchorId = firstVisibleElement.setting.key;1648} else if (firstVisibleElement instanceof SettingsTreeGroupElement) {1649anchorId = firstVisibleElement.id;1650}16511652if (this.settingsTreeModel.value) {1653this.refreshModels(resolvedSettingsRoot);16541655if (triggerSearch && this.searchResultModel) {1656// If an extension's settings were just loaded and a search is active, retrigger the search so it shows up1657return await this.onSearchInputChanged(false);1658}16591660this.refreshTOCTree();1661this.renderTree(undefined, forceRefresh);16621663if (anchorId) {1664const newModel = this.settingsTreeModel.value;1665let newElement: SettingsTreeElement | undefined;16661667// eslint-disable-next-line no-restricted-syntax1668const settings = newModel.getElementsByName(anchorId);1669if (settings && settings.length > 0) {1670newElement = settings[0];1671} else {1672const findGroup = (roots: SettingsTreeGroupElement[]): SettingsTreeGroupElement | undefined => {1673for (const g of roots) {1674if (g.id === anchorId) {1675return g;1676}1677if (g.children) {1678for (const child of g.children) {1679if (child instanceof SettingsTreeGroupElement) {1680const found = findGroup([child]);1681if (found) {1682return found;1683}1684}1685}1686}1687}1688return undefined;1689};1690newElement = findGroup([newModel.root]);1691}16921693if (newElement) {1694try {1695this.settingsTree.reveal(newElement, 0);1696} catch (e) {1697// Ignore the error1698}1699}1700}1701} else {1702this.settingsTreeModel.value = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());1703this.refreshModels(resolvedSettingsRoot);17041705// Don't restore the cached state if we already have a query value from calling _setOptions().1706const cachedState = !this.viewState.query ? this.restoreCachedState() : undefined;1707if (cachedState?.searchQuery || this.searchWidget.getValue()) {1708await this.onSearchInputChanged(true);1709} else {1710this.refreshTOCTree();17111712// In paginated mode, set initial category to the first one (Commonly Used)1713const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY);1714if (scrollBehavior === 'paginated') {1715const rootChildren = this.settingsTreeModel.value.root.children;1716if (Array.isArray(rootChildren) && rootChildren.length > 0) {1717const firstCategory = rootChildren[0];1718if (firstCategory instanceof SettingsTreeGroupElement) {1719this.viewState.categoryFilter = firstCategory;1720this.tocTree.setFocus([firstCategory]);1721this.tocTree.setSelection([firstCategory]);1722}1723}1724}17251726this.refreshTree();1727this.tocTree.collapseAll();1728}1729}1730}17311732private updateElementsByKey(keys: ReadonlySet<string>): void {1733if (keys.size) {1734if (this.searchResultModel) {1735keys.forEach(key => this.searchResultModel!.updateElementsByName(key));1736}17371738if (this.settingsTreeModel.value) {1739keys.forEach(key => this.settingsTreeModel.value!.updateElementsByName(key));1740}17411742keys.forEach(key => this.renderTree(key));1743} else {1744this.renderTree();1745}1746}17471748private getActiveControlInSettingsTree(): HTMLElement | null {1749const element = this.settingsTree.getHTMLElement();1750const activeElement = element.ownerDocument.activeElement;1751return (activeElement && DOM.isAncestorOfActiveElement(element)) ?1752<HTMLElement>activeElement :1753null;1754}17551756private renderTree(key?: string, force = false): void {1757if (!force && key && this.scheduledRefreshes.has(key)) {1758this.updateModifiedLabelForKey(key);1759return;1760}17611762// If the context view is focused, delay rendering settings1763if (this.contextViewFocused()) {1764// eslint-disable-next-line no-restricted-syntax1765const element = this.window.document.querySelector('.context-view');1766if (element) {1767this.scheduleRefresh(element as HTMLElement, key);1768}1769return;1770}17711772// If a setting control is currently focused, schedule a refresh for later1773const activeElement = this.getActiveControlInSettingsTree();1774const focusedSetting = activeElement && this.settingRenderers.getSettingDOMElementForDOMElement(activeElement);1775if (focusedSetting && !force) {1776// If a single setting is being refreshed, it's ok to refresh now if that is not the focused setting1777if (key) {1778const focusedKey = focusedSetting.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR);1779if (focusedKey === key &&1780// update `list`s live, as they have a separate "submit edit" step built in before this1781(focusedSetting.parentElement && !focusedSetting.parentElement.classList.contains('setting-item-list'))1782) {1783this.updateModifiedLabelForKey(key);1784this.scheduleRefresh(focusedSetting, key);1785return;1786}1787} else {1788this.scheduleRefresh(focusedSetting);1789return;1790}1791}17921793this.renderResultCountMessages(false);17941795if (key) {1796// eslint-disable-next-line no-restricted-syntax1797const elements = this.currentSettingsModel?.getElementsByName(key);1798if (elements?.length) {1799if (elements.length >= 2) {1800console.warn('More than one setting with key ' + key + ' found');1801}1802this.refreshSingleElement(elements[0]);1803} else {1804// Refresh requested for a key that we don't know about1805return;1806}1807} else {1808this.refreshTree();1809}18101811return;1812}18131814private contextViewFocused(): boolean {1815return !!DOM.findParentWithClass(<HTMLElement>this.rootElement.ownerDocument.activeElement, 'context-view');1816}18171818private refreshSingleElement(element: SettingsTreeSettingElement): void {1819if (this.isVisible()1820&& this.settingsTree.hasElement(element)1821&& (!element.setting.deprecationMessage || element.isConfigured)) {1822this.settingsTree.rerender(element);1823}1824}18251826private refreshTree(): void {1827if (this.isVisible() && this.currentSettingsModel) {1828this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root));1829}1830}18311832private refreshTOCTree(): void {1833if (this.isVisible()) {1834this.tocTreeModel.update();1835this.tocTree.setChildren(null, createTOCIterator(this.tocTreeModel, this.tocTree));1836}1837}18381839private updateModifiedLabelForKey(key: string): void {1840if (!this.currentSettingsModel) {1841return;1842}1843// eslint-disable-next-line no-restricted-syntax1844const dataElements = this.currentSettingsModel.getElementsByName(key);1845const isModified = dataElements && dataElements[0] && dataElements[0].isConfigured; // all elements are either configured or not1846const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key);1847if (elements && elements[0]) {1848elements[0].classList.toggle('is-configured', !!isModified);1849}1850}18511852private async onSearchInputChanged(expandResults: boolean): Promise<void> {1853if (!this.currentSettingsModel) {1854// Initializing search widget value1855return;1856}18571858const query = this.searchWidget.getValue().trim();1859this.viewState.query = query;1860await this.triggerSearch(query.replace(/\u203A/g, ' '), expandResults);1861}18621863private parseSettingFromJSON(query: string): string | null {1864const match = query.match(/"([a-zA-Z.]+)": /);1865return match && match[1];1866}18671868/**1869* Toggles the visibility of the Settings editor table of contents during a search1870* depending on the behavior.1871*/1872private toggleTocBySearchBehaviorType() {1873const tocBehavior = this.configurationService.getValue<'filter' | 'hide'>(SEARCH_TOC_BEHAVIOR_KEY);1874const hideToc = tocBehavior === 'hide';1875if (hideToc) {1876this.splitView.setViewVisible(0, false);1877this.splitView.style({1878separatorBorder: Color.transparent1879});1880} else {1881this.layoutSplitView(this.dimension);1882}1883}18841885private async triggerSearch(query: string, expandResults: boolean): Promise<void> {1886const progressRunner = this.editorProgressService.show(true, 800);1887const showAdvanced = this.viewState.tagFilters?.has(ADVANCED_SETTING_TAG);1888this.viewState.tagFilters = new Set<string>();1889this.viewState.extensionFilters = new Set<string>();1890this.viewState.featureFilters = new Set<string>();1891this.viewState.idFilters = new Set<string>();1892this.viewState.languageFilter = undefined;1893if (query) {1894const parsedQuery = parseQuery(query);1895query = parsedQuery.query;1896parsedQuery.tags.forEach(tag => this.viewState.tagFilters!.add(tag));1897parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId));1898parsedQuery.featureFilters.forEach(feature => this.viewState.featureFilters!.add(feature));1899parsedQuery.idFilters.forEach(id => this.viewState.idFilters!.add(id));1900this.viewState.languageFilter = parsedQuery.languageFilter;1901}19021903if (showAdvanced !== this.viewState.tagFilters?.has(ADVANCED_SETTING_TAG)) {1904await this.onConfigUpdate();1905}19061907this.settingsTargetsWidget.updateLanguageFilterIndicators(this.viewState.languageFilter);19081909if (query && query !== '@') {1910query = this.parseSettingFromJSON(query) || query;1911await this.triggerFilterPreferences(query, expandResults, progressRunner);1912this.toggleTocBySearchBehaviorType();1913} else {1914if (this.viewState.tagFilters.size || this.viewState.extensionFilters.size || this.viewState.featureFilters.size || this.viewState.idFilters.size || this.viewState.languageFilter) {1915this.searchResultModel = this.createFilterModel();1916} else {1917this.searchResultModel = null;1918}19191920this.searchDelayer.cancel();1921if (this.searchInProgress) {1922this.searchInProgress.dispose(true);1923this.searchInProgress = null;1924}19251926if (expandResults) {1927this.tocTree.setFocus([]);1928this.viewState.categoryFilter = undefined;1929}1930this.tocTreeModel.currentSearchModel = this.searchResultModel;19311932if (this.searchResultModel) {1933// Added a filter model1934if (expandResults) {1935this.tocTree.setSelection([]);1936this.tocTree.expandAll();1937}1938this.refreshTOCTree();1939this.renderResultCountMessages(false);1940this.refreshTree();1941this.toggleTocBySearchBehaviorType();1942} else if (!this.tocTreeDisposed) {1943// Leaving search mode1944this.tocTree.collapseAll();1945this.refreshTOCTree();1946this.renderResultCountMessages(false);1947this.refreshTree();1948this.layoutSplitView(this.dimension);1949}1950progressRunner.done();1951}1952}19531954/**1955* Return a fake SearchResultModel which can hold a flat list of all settings, to be filtered (@modified etc)1956*/1957private createFilterModel(): SearchResultModel {1958const filterModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted());19591960const fullResult: ISearchResult = {1961filterMatches: [],1962exactMatch: false,1963};1964const shouldShowAdvanced = this.canShowAdvancedSettings();1965for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) {1966for (const sect of g.sections) {1967for (const setting of sect.settings) {1968if (!shouldShowAdvanced && !this.shouldShowSetting(setting)) {1969continue;1970}1971fullResult.filterMatches.push({1972setting,1973matches: [],1974matchType: SettingMatchType.None,1975keyMatchScore: 0,1976score: 0,1977providerName: FILTER_MODEL_SEARCH_PROVIDER_NAME1978});1979}1980}1981}19821983filterModel.setResult(0, fullResult);1984return filterModel;1985}19861987private async triggerFilterPreferences(query: string, expandResults: boolean, progressRunner: IProgressRunner): Promise<void> {1988if (this.searchInProgress) {1989this.searchInProgress.dispose(true);1990this.searchInProgress = null;1991}19921993const searchInProgress = this.searchInProgress = new CancellationTokenSource();1994return this.searchDelayer.trigger(async () => {1995if (searchInProgress.token.isCancellationRequested) {1996return;1997}1998this.disableAiSearchToggle();1999const localResults = await this.doLocalSearch(query, searchInProgress.token);2000if (!this.searchResultModel || searchInProgress.token.isCancellationRequested) {2001return;2002}2003this.searchResultModel.showAiResults = false;20042005if (localResults && localResults.filterMatches.length > 0) {2006// The remote results might take a while and2007// are always appended to the end anyway, so2008// show some results now.2009this.onDidFinishSearch(expandResults, undefined);2010}20112012if (!localResults || !localResults.exactMatch) {2013await this.doRemoteSearch(query, searchInProgress.token);2014}2015if (searchInProgress.token.isCancellationRequested) {2016return;2017}20182019if (this.aiSearchPromise) {2020this.aiSearchPromise.cancel();2021}20222023// Kick off an AI search in the background if the toggle is shown.2024// We purposely do not await it.2025if (this.searchInputActionBar && this.showAiResultsAction && this.searchInputActionBar.hasAction(this.showAiResultsAction)) {2026this.aiSearchPromise = createCancelablePromise(token => {2027return this.doAiSearch(query, token).then((results) => {2028if (results && this.showAiResultsAction) {2029this.showAiResultsAction.enabled = true;2030this.aiResultsAvailable.set(true);2031this.showAiResultsAction.label = SHOW_AI_RESULTS_ENABLED_LABEL;2032this.renderResultCountMessages(true);2033}2034}).catch(e => {2035if (!isCancellationError(e)) {2036this.logService.trace('Error during AI settings search:', e);2037}2038});2039});2040}20412042this.onDidFinishSearch(expandResults, progressRunner);2043});2044}20452046private onDidFinishSearch(expandResults: boolean, progressRunner: IProgressRunner | undefined): void {2047this.tocTreeModel.currentSearchModel = this.searchResultModel;2048if (expandResults) {2049this.tocTree.setFocus([]);2050this.viewState.categoryFilter = undefined;2051this.tocTree.expandAll();2052this.settingsTree.scrollTop = 0;2053}2054this.refreshTOCTree();2055this.renderTree(undefined, true);2056progressRunner?.done();2057}20582059private doLocalSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {2060const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);2061return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, STRING_MATCH_SEARCH_PROVIDER_NAME, token);2062}20632064private doRemoteSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {2065const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);2066if (!remoteSearchProvider) {2067return Promise.resolve(null);2068}2069return this.searchWithProvider(SearchResultIdx.Remote, remoteSearchProvider, TF_IDF_SEARCH_PROVIDER_NAME, token);2070}20712072private async doAiSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {2073const aiSearchProvider = this.preferencesSearchService.getAiSearchProvider(query);2074if (!aiSearchProvider) {2075return null;2076}20772078const embeddingsResults = await this.searchWithProvider(SearchResultIdx.Embeddings, aiSearchProvider, EMBEDDINGS_SEARCH_PROVIDER_NAME, token);2079if (!embeddingsResults || token.isCancellationRequested) {2080return null;2081}20822083const llmResults = await this.getLLMRankedResults(query, token);2084if (token.isCancellationRequested) {2085return null;2086}20872088return {2089filterMatches: embeddingsResults.filterMatches.concat(llmResults?.filterMatches ?? []),2090exactMatch: false2091};2092}20932094private async getLLMRankedResults(query: string, token: CancellationToken): Promise<ISearchResult | null> {2095const aiSearchProvider = this.preferencesSearchService.getAiSearchProvider(query);2096if (!aiSearchProvider) {2097return null;2098}20992100this.stopWatch.reset();2101const result = await aiSearchProvider.getLLMRankedResults(token);2102this.stopWatch.stop();21032104if (token.isCancellationRequested) {2105return null;2106}21072108// Only log the elapsed time if there are actual results.2109if (result && result.filterMatches.length > 0) {2110const elapsed = this.stopWatch.elapsed();2111this.logSearchPerformance(LLM_RANKED_SEARCH_PROVIDER_NAME, elapsed);2112}21132114this.searchResultModel!.setResult(SearchResultIdx.AiSelected, result);2115return result;2116}21172118private async searchWithProvider(type: SearchResultIdx, searchProvider: ISearchProvider, providerName: string, token: CancellationToken): Promise<ISearchResult | null> {2119this.stopWatch.reset();2120const result = await this._searchPreferencesModel(this.defaultSettingsEditorModel, searchProvider, token);2121this.stopWatch.stop();21222123if (token.isCancellationRequested) {2124// Handle cancellation like this because cancellation is lost inside the search provider due to async/await2125return null;2126}21272128// Filter out advanced settings unless the advanced tag is explicitly set or setting matches an ID filter2129if (result && !this.canShowAdvancedSettings()) {2130result.filterMatches = result.filterMatches.filter(match => this.shouldShowSetting(match.setting));2131}21322133// Only log the elapsed time if there are actual results.2134if (result && result.filterMatches.length > 0) {2135const elapsed = this.stopWatch.elapsed();2136this.logSearchPerformance(providerName, elapsed);2137}21382139this.searchResultModel ??= this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted());2140this.searchResultModel.setResult(type, result);2141return result;2142}21432144private logSearchPerformance(providerName: string, elapsed: number): void {2145type SettingsEditorSearchPerformanceEvent = {2146providerName: string | undefined;2147elapsedMs: number;2148};2149type SettingsEditorSearchPerformanceClassification = {2150providerName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the search provider, if applicable.' };2151elapsedMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The time taken to perform the search, in milliseconds.' };2152owner: 'rzhao271';2153comment: 'Event emitted when the Settings editor calls a search provider to search for a setting';2154};2155this.telemetryService.publicLog2<SettingsEditorSearchPerformanceEvent, SettingsEditorSearchPerformanceClassification>('settingsEditor.searchPerformance', {2156providerName,2157elapsedMs: elapsed,2158});2159}21602161private renderResultCountMessages(showAiResultsMessage: boolean) {2162if (!this.currentSettingsModel) {2163return;2164}21652166this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 02167? 'initial'2168: 'none';21692170if (!this.searchResultModel) {2171if (this.countElement.style.display !== 'none') {2172this.searchResultLabel = null;2173this.updateInputAriaLabel();2174this.countElement.style.display = 'none';2175this.countElement.innerText = '';2176this.layout(this.dimension);2177}21782179this.rootElement.classList.remove('no-results');2180this.splitView.el.style.visibility = 'visible';2181return;2182} else {2183const count = this.searchResultModel.getUniqueResultsCount();2184let resultString: string;21852186if (showAiResultsMessage) {2187switch (count) {2188case 0: resultString = localize('noResultsWithAiAvailable', "No Settings Found. AI Results Available"); break;2189case 1: resultString = localize('oneResultWithAiAvailable', "1 Setting Found. AI Results Available"); break;2190default: resultString = localize('moreThanOneResultWithAiAvailable', "{0} Settings Found. AI Results Available", count);2191}2192} else {2193switch (count) {2194case 0: resultString = localize('noResults', "No Settings Found"); break;2195case 1: resultString = localize('oneResult', "1 Setting Found"); break;2196default: resultString = localize('moreThanOneResult', "{0} Settings Found", count);2197}2198}21992200this.searchResultLabel = resultString;2201this.updateInputAriaLabel();2202this.countElement.innerText = resultString;2203aria.status(resultString);22042205if (this.countElement.style.display !== 'block') {2206this.countElement.style.display = 'block';2207}2208this.layout(this.dimension);2209this.rootElement.classList.toggle('no-results', count === 0);2210this.splitView.el.style.visibility = count === 0 ? 'hidden' : 'visible';2211}2212}22132214private async _searchPreferencesModel(model: ISettingsEditorModel, provider: ISearchProvider, token: CancellationToken): Promise<ISearchResult | null> {2215try {2216return await provider.searchModel(model, token);2217} catch (err) {2218if (isCancellationError(err)) {2219return Promise.reject(err);2220} else {2221return null;2222}2223}2224}22252226private layoutSplitView(dimension: DOM.Dimension): void {2227if (!this.isVisible()) {2228return;2229}2230const listHeight = dimension.height - (72 + 11 + 14 /* header height + editor padding */);22312232this.splitView.el.style.height = `${listHeight}px`;22332234// We call layout first so the splitView has an idea of how much2235// space it has, otherwise setViewVisible results in the first panel2236// showing up at the minimum size whenever the Settings editor2237// opens for the first time.2238this.splitView.layout(this.bodyContainer.clientWidth, listHeight);22392240const tocBehavior = this.configurationService.getValue<'filter' | 'hide'>(SEARCH_TOC_BEHAVIOR_KEY);2241const hideTocForSearch = tocBehavior === 'hide' && this.searchResultModel;2242if (!hideTocForSearch) {2243const firstViewWasVisible = this.splitView.isViewVisible(0);2244const firstViewVisible = this.bodyContainer.clientWidth >= SettingsEditor2.NARROW_TOTAL_WIDTH;22452246this.splitView.setViewVisible(0, firstViewVisible);2247// If the first view is again visible, and we have enough space, immediately set the2248// editor to use the reset width rather than the cached min width2249if (!firstViewWasVisible && firstViewVisible && this.bodyContainer.clientWidth >= SettingsEditor2.EDITOR_MIN_WIDTH + SettingsEditor2.TOC_RESET_WIDTH) {2250this.splitView.resizeView(0, SettingsEditor2.TOC_RESET_WIDTH);2251}2252this.splitView.style({2253separatorBorder: firstViewVisible ? this.theme.getColor(settingsSashBorder)! : Color.transparent2254});2255}2256}22572258protected override saveState(): void {2259if (this.isVisible()) {2260const searchQuery = this.searchWidget.getValue().trim();2261const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget;2262if (this.input) {2263this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target });2264}2265} else if (this.input) {2266this.editorMemento.clearEditorState(this.input, this.group);2267}22682269super.saveState();2270}2271}22722273class SyncControls extends Disposable {2274private readonly lastSyncedLabel!: HTMLElement;2275private readonly turnOnSyncButton!: Button;22762277private readonly _onDidChangeLastSyncedLabel = this._register(new Emitter<string>());2278public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event;22792280constructor(2281window: CodeWindow,2282container: HTMLElement,2283@ICommandService private readonly commandService: ICommandService,2284@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,2285@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,2286@ITelemetryService telemetryService: ITelemetryService,2287) {2288super();22892290const turnOnSyncButtonContainer = DOM.append(container, $('.turn-on-sync'));2291this.turnOnSyncButton = this._register(new Button(turnOnSyncButtonContainer, { title: true, ...defaultButtonStyles }));2292this.lastSyncedLabel = DOM.append(container, $('.last-synced-label'));2293DOM.hide(this.lastSyncedLabel);22942295this.turnOnSyncButton.enabled = true;2296this.turnOnSyncButton.label = localize('turnOnSyncButton', "Backup and Sync Settings");2297DOM.hide(this.turnOnSyncButton.element);22982299this._register(this.turnOnSyncButton.onDidClick(async () => {2300await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn');2301}));23022303this.updateLastSyncedTime();2304this._register(this.userDataSyncService.onDidChangeLastSyncTime(() => {2305this.updateLastSyncedTime();2306}));23072308const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer());2309updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window);23102311this.update();2312this._register(this.userDataSyncService.onDidChangeStatus(() => {2313this.update();2314}));23152316this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => {2317this.update();2318}));2319}23202321private updateLastSyncedTime(): void {2322const last = this.userDataSyncService.lastSyncTime;2323let label: string;2324if (typeof last === 'number') {2325const d = fromNow(last, true, undefined, true);2326label = localize('lastSyncedLabel', "Last synced: {0}", d);2327} else {2328label = '';2329}23302331this.lastSyncedLabel.textContent = label;2332this._onDidChangeLastSyncedLabel.fire(label);2333}23342335private update(): void {2336if (this.userDataSyncService.status === SyncStatus.Uninitialized) {2337return;2338}23392340if (this.userDataSyncEnablementService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) {2341DOM.show(this.lastSyncedLabel);2342DOM.hide(this.turnOnSyncButton.element);2343} else {2344DOM.hide(this.lastSyncedLabel);2345DOM.show(this.turnOnSyncButton.element);2346}2347}2348}23492350interface ISettingsEditor2State {2351searchQuery: string;2352target: SettingsTarget;2353}235423552356