Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts
3296 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 { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js';54import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';55import { IExtensionService } from '../../../services/extensions/common/extensions.js';56import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsEditorOptions, ISettingsGroup, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from '../../../services/preferences/common/preferences.js';57import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js';58import { nullRange, Settings2EditorModel } from '../../../services/preferences/common/preferencesModels.js';59import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';60import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js';61import { SuggestEnabledInput } from '../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js';62import { 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';63import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from '../common/settingsEditorColorRegistry.js';64import './media/settingsEditor2.css';65import { preferencesAiResultsIcon, preferencesClearInputIcon, preferencesFilterIcon } from './preferencesIcons.js';66import { SettingsTarget, SettingsTargetsWidget } from './preferencesWidgets.js';67import { ISettingOverrideClickEvent } from './settingsEditorSettingIndicators.js';68import { getCommonlyUsedData, ITOCEntry, tocData } from './settingsLayout.js';69import { SettingsSearchFilterDropdownMenuActionViewItem } from './settingsSearchMenu.js';70import { AbstractSettingRenderer, createTocTreeForExtensionSettings, HeightChangeParams, ISettingLinkClickEvent, resolveConfiguredUntrustedSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from './settingsTree.js';71import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from './settingsTreeModels.js';72import { createTOCIterator, TOCTree, TOCTreeModel } from './tocTree.js';7374export const enum SettingsFocusContext {75Search,76TableOfContents,77SettingTree,78SettingControl79}8081export function createGroupIterator(group: SettingsTreeGroupElement): Iterable<ITreeElement<SettingsTreeGroupChild>> {82return Iterable.map(group.children, g => {83return {84element: g,85children: g instanceof SettingsTreeGroupElement ?86createGroupIterator(g) :87undefined88};89});90}9192const $ = DOM.$;9394interface IFocusEventFromScroll extends KeyboardEvent {95fromScroll: true;96}9798const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings");99const SEARCH_TOC_BEHAVIOR_KEY = 'workbench.settings.settingsSearchTocBehavior';100101const SHOW_AI_RESULTS_ENABLED_LABEL = localize('showAiResultsEnabled', "Show AI-recommended results");102const SHOW_AI_RESULTS_DISABLED_LABEL = localize('showAiResultsDisabled', "No AI results available at this time...");103104const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState';105106export class SettingsEditor2 extends EditorPane {107108static readonly ID: string = 'workbench.editor.settings2';109private static NUM_INSTANCES: number = 0;110private static SEARCH_DEBOUNCE: number = 200;111private static SETTING_UPDATE_FAST_DEBOUNCE: number = 200;112private static SETTING_UPDATE_SLOW_DEBOUNCE: number = 1000;113private static CONFIG_SCHEMA_UPDATE_DELAYER = 500;114private static TOC_MIN_WIDTH: number = 100;115private static TOC_RESET_WIDTH: number = 200;116private static EDITOR_MIN_WIDTH: number = 500;117// Below NARROW_TOTAL_WIDTH, we only render the editor rather than the ToC.118private static NARROW_TOTAL_WIDTH: number = this.TOC_RESET_WIDTH + this.EDITOR_MIN_WIDTH;119120private static SUGGESTIONS: string[] = [121`@${MODIFIED_SETTING_TAG}`,122'@tag:notebookLayout',123'@tag:notebookOutputLayout',124`@tag:${REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG}`,125`@tag:${WORKSPACE_TRUST_SETTING_TAG}`,126'@tag:sync',127'@tag:usesOnlineServices',128'@tag:telemetry',129'@tag:accessibility',130'@tag:preview',131'@tag:experimental',132`@${ID_SETTING_TAG}`,133`@${EXTENSION_SETTING_TAG}`,134`@${FEATURE_SETTING_TAG}scm`,135`@${FEATURE_SETTING_TAG}explorer`,136`@${FEATURE_SETTING_TAG}search`,137`@${FEATURE_SETTING_TAG}debug`,138`@${FEATURE_SETTING_TAG}extensions`,139`@${FEATURE_SETTING_TAG}terminal`,140`@${FEATURE_SETTING_TAG}task`,141`@${FEATURE_SETTING_TAG}problems`,142`@${FEATURE_SETTING_TAG}output`,143`@${FEATURE_SETTING_TAG}comments`,144`@${FEATURE_SETTING_TAG}remote`,145`@${FEATURE_SETTING_TAG}timeline`,146`@${FEATURE_SETTING_TAG}notebook`,147`@${POLICY_SETTING_TAG}`148];149150private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean {151if (Array.isArray(type)) {152// nullable integer/number or complex153return false;154}155return type === SettingValueType.Enum ||156type === SettingValueType.Array ||157type === SettingValueType.BooleanObject ||158type === SettingValueType.Object ||159type === SettingValueType.Complex ||160type === SettingValueType.Boolean ||161type === SettingValueType.Exclude ||162type === SettingValueType.Include;163}164165// (!) Lots of props that are set once on the first render166private defaultSettingsEditorModel!: Settings2EditorModel;167private readonly modelDisposables: DisposableStore;168169private rootElement!: HTMLElement;170private headerContainer!: HTMLElement;171private searchContainer: HTMLElement | null = null;172private bodyContainer!: HTMLElement;173private searchWidget!: SuggestEnabledInput;174private countElement!: HTMLElement;175private controlsElement!: HTMLElement;176private settingsTargetsWidget!: SettingsTargetsWidget;177178private splitView!: SplitView<number>;179180private settingsTreeContainer!: HTMLElement;181private settingsTree!: SettingsTree;182private settingRenderers!: SettingTreeRenderers;183private tocTreeModel!: TOCTreeModel;184private readonly settingsTreeModel = this._register(new MutableDisposable<SettingsTreeModel>());185private noResultsMessage!: HTMLElement;186private clearFilterLinkContainer!: HTMLElement;187188private tocTreeContainer!: HTMLElement;189private tocTree!: TOCTree;190191private searchDelayer: Delayer<void>;192private searchInProgress: CancellationTokenSource | null = null;193private aiSearchPromise: CancelablePromise<void> | null = null;194195private stopWatch: StopWatch;196197private showAiResultsAction: Action | null = null;198199private searchInputDelayer: Delayer<void>;200private updatedConfigSchemaDelayer: Delayer<void>;201202private settingFastUpdateDelayer: Delayer<void>;203private settingSlowUpdateDelayer: Delayer<void>;204private pendingSettingUpdate: { key: string; value: any; languageFilter: string | undefined } | null = null;205206private readonly viewState: ISettingsEditorViewState;207private readonly _searchResultModel = this._register(new MutableDisposable<SearchResultModel>());208private searchResultLabel: string | null = null;209private lastSyncedLabel: string | null = null;210private settingsOrderByTocIndex: Map<string, number> | null = null;211212private tocRowFocused: IContextKey<boolean>;213private settingRowFocused: IContextKey<boolean>;214private inSettingsEditorContextKey: IContextKey<boolean>;215private searchFocusContextKey: IContextKey<boolean>;216private aiResultsAvailable: IContextKey<boolean>;217218private scheduledRefreshes: Map<string, DisposableStore>;219private _currentFocusContext: SettingsFocusContext = SettingsFocusContext.Search;220221/** Don't spam warnings */222private hasWarnedMissingSettings = false;223private tocTreeDisposed = false;224225/** Persist the search query upon reloads */226private editorMemento: IEditorMemento<ISettingsEditor2State>;227228private tocFocusedElement: SettingsTreeGroupElement | null = null;229private treeFocusedElement: SettingsTreeElement | null = null;230private settingsTreeScrollTop = 0;231private dimension!: DOM.Dimension;232233private installedExtensionIds: string[] = [];234private dismissedExtensionSettings: string[] = [];235236private readonly DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY = 'settingsEditor2.dismissedExtensionSettings';237private readonly DISMISSED_EXTENSION_SETTINGS_DELIMITER = '\t';238239private readonly inputChangeListener: MutableDisposable<IDisposable>;240241private searchInputActionBar: ActionBar | null = null;242243constructor(244group: IEditorGroup,245@ITelemetryService telemetryService: ITelemetryService,246@IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService,247@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,248@IThemeService themeService: IThemeService,249@IPreferencesService private readonly preferencesService: IPreferencesService,250@IInstantiationService private readonly instantiationService: IInstantiationService,251@IPreferencesSearchService private readonly preferencesSearchService: IPreferencesSearchService,252@ILogService private readonly logService: ILogService,253@IContextKeyService private readonly contextKeyService: IContextKeyService,254@IStorageService private readonly storageService: IStorageService,255@IEditorGroupsService protected editorGroupService: IEditorGroupsService,256@IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService,257@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,258@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,259@IExtensionService private readonly extensionService: IExtensionService,260@ILanguageService private readonly languageService: ILanguageService,261@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,262@IProductService private readonly productService: IProductService,263@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,264@IEditorProgressService private readonly editorProgressService: IEditorProgressService,265@IUserDataProfileService userDataProfileService: IUserDataProfileService,266@IKeybindingService private readonly keybindingService: IKeybindingService267) {268super(SettingsEditor2.ID, group, telemetryService, themeService, storageService);269this.searchDelayer = new Delayer(200);270this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL };271272this.settingFastUpdateDelayer = new Delayer<void>(SettingsEditor2.SETTING_UPDATE_FAST_DEBOUNCE);273this.settingSlowUpdateDelayer = new Delayer<void>(SettingsEditor2.SETTING_UPDATE_SLOW_DEBOUNCE);274275this.searchInputDelayer = new Delayer<void>(SettingsEditor2.SEARCH_DEBOUNCE);276this.updatedConfigSchemaDelayer = new Delayer<void>(SettingsEditor2.CONFIG_SCHEMA_UPDATE_DELAYER);277278this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);279this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);280this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService);281this.settingRowFocused = CONTEXT_SETTINGS_ROW_FOCUS.bindTo(contextKeyService);282this.aiResultsAvailable = CONTEXT_AI_SETTING_RESULTS_AVAILABLE.bindTo(contextKeyService);283284this.scheduledRefreshes = new Map<string, DisposableStore>();285this.stopWatch = new StopWatch(false);286287this.editorMemento = this.getEditorMemento<ISettingsEditor2State>(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY);288289this.dismissedExtensionSettings = this.storageService290.get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '')291.split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER);292293this._register(configurationService.onDidChangeConfiguration(e => {294if (e.affectedKeys.has(WorkbenchSettingsEditorSettings.ShowAISearchToggle)295|| e.affectedKeys.has(WorkbenchSettingsEditorSettings.EnableNaturalLanguageSearch)296|| e.affectedKeys.has('chat.disableAIFeatures')) {297this.updateAiSearchToggleVisibility();298}299if (e.source !== ConfigurationTarget.DEFAULT) {300this.onConfigUpdate(e.affectedKeys);301}302}));303304this._register(userDataProfileService.onDidChangeCurrentProfile(e => {305e.join(this.whenCurrentProfileChanged());306}));307308this._register(workspaceTrustManagementService.onDidChangeTrust(() => {309this.searchResultModel?.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());310311if (this.settingsTreeModel.value) {312this.settingsTreeModel.value.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());313this.renderTree();314}315}));316317this._register(configurationService.onDidChangeRestrictedSettings(e => {318if (e.default.length && this.currentSettingsModel) {319this.updateElementsByKey(new Set(e.default));320}321}));322323this._register(extensionManagementService.onDidInstallExtensions(() => {324this.refreshInstalledExtensionsList();325}));326this._register(extensionManagementService.onDidUninstallExtension(() => {327this.refreshInstalledExtensionsList();328}));329330this.modelDisposables = this._register(new DisposableStore());331332if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) {333SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`);334}335this.inputChangeListener = this._register(new MutableDisposable());336}337338private async whenCurrentProfileChanged(): Promise<void> {339this.updatedConfigSchemaDelayer.trigger(() => {340this.dismissedExtensionSettings = this.storageService341.get(this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY, StorageScope.PROFILE, '')342.split(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER);343this.onConfigUpdate(undefined, true);344});345}346347private disableAiSearchToggle(): void {348if (this.showAiResultsAction) {349this.showAiResultsAction.checked = false;350this.showAiResultsAction.enabled = false;351this.aiResultsAvailable.set(false);352this.showAiResultsAction.label = SHOW_AI_RESULTS_DISABLED_LABEL;353}354}355356private updateAiSearchToggleVisibility(): void {357if (!this.searchContainer || !this.showAiResultsAction || !this.searchInputActionBar) {358return;359}360361const showAiToggle = this.configurationService.getValue<boolean>(WorkbenchSettingsEditorSettings.ShowAISearchToggle);362const enableNaturalLanguageSearch = this.configurationService.getValue<boolean>(WorkbenchSettingsEditorSettings.EnableNaturalLanguageSearch);363const chatSetupHidden = this.contextKeyService.getContextKeyValue<boolean>('chatSetupHidden');364const chatFeaturesDisabled = this.configurationService.getValue<boolean>('chat.disableAIFeatures');365const canShowToggle = showAiToggle && enableNaturalLanguageSearch && !chatSetupHidden && !chatFeaturesDisabled;366367const alreadyVisible = this.searchInputActionBar.hasAction(this.showAiResultsAction);368if (!alreadyVisible && canShowToggle) {369this.searchInputActionBar.push(this.showAiResultsAction, {370index: 0,371label: false,372icon: true373});374this.searchContainer.classList.add('with-ai-toggle');375} else if (alreadyVisible) {376this.searchInputActionBar.pull(0);377this.searchContainer.classList.remove('with-ai-toggle');378this.showAiResultsAction.checked = false;379}380}381382override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; }383override get maximumWidth(): number { return Number.POSITIVE_INFINITY; }384override get minimumHeight() { return 180; }385386// these setters need to exist because this extends from EditorPane387override set minimumWidth(value: number) { /*noop*/ }388override set maximumWidth(value: number) { /*noop*/ }389390private get currentSettingsModel(): SettingsTreeModel | undefined {391return this.searchResultModel || this.settingsTreeModel.value;392}393394private get searchResultModel(): SearchResultModel | null {395return this._searchResultModel.value ?? null;396}397398private set searchResultModel(value: SearchResultModel | null) {399this._searchResultModel.value = value ?? undefined;400401this.rootElement.classList.toggle('search-mode', !!this._searchResultModel.value);402}403404private get focusedSettingDOMElement(): HTMLElement | undefined {405const focused = this.settingsTree.getFocus()[0];406if (!(focused instanceof SettingsTreeSettingElement)) {407return;408}409410return this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), focused.setting.key)[0];411}412413get currentFocusContext() {414return this._currentFocusContext;415}416417protected createEditor(parent: HTMLElement): void {418parent.setAttribute('tabindex', '-1');419this.rootElement = DOM.append(parent, $('.settings-editor', { tabindex: '-1' }));420421this.createHeader(this.rootElement);422this.createBody(this.rootElement);423this.addCtrlAInterceptor(this.rootElement);424this.updateStyles();425426this._register(registerNavigableContainer({427name: 'settingsEditor2',428focusNotifiers: [this],429focusNextWidget: () => {430if (this.searchWidget.inputWidget.hasWidgetFocus()) {431this.focusTOC();432}433},434focusPreviousWidget: () => {435if (!this.searchWidget.inputWidget.hasWidgetFocus()) {436this.focusSearch();437}438}439}));440}441442override async setInput(input: SettingsEditor2Input, options: ISettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {443this.inSettingsEditorContextKey.set(true);444await super.setInput(input, options, context, token);445if (!this.input) {446return;447}448449const model = await this.input.resolve();450if (token.isCancellationRequested || !(model instanceof Settings2EditorModel)) {451return;452}453454this.modelDisposables.clear();455this.modelDisposables.add(model.onDidChangeGroups(() => {456this.updatedConfigSchemaDelayer.trigger(() => {457this.onConfigUpdate(undefined, false, true);458});459}));460this.defaultSettingsEditorModel = model;461462options = options || validateSettingsEditorOptions({});463if (!this.viewState.settingsTarget || !this.settingsTargetsWidget.settingsTarget) {464const optionsHasViewStateTarget = options.viewState && (options.viewState as ISettingsEditorViewState).settingsTarget;465if (!options.target && !optionsHasViewStateTarget) {466options.target = ConfigurationTarget.USER_LOCAL;467}468}469this._setOptions(options);470471// Don't block setInput on render (which can trigger an async search)472this.onConfigUpdate(undefined, true).then(() => {473// This event runs when the editor closes.474this.inputChangeListener.value = input.onWillDispose(() => {475this.searchWidget.setValue('');476});477478// Init TOC selection479this.updateTreeScrollSync();480});481482await this.refreshInstalledExtensionsList();483}484485private async refreshInstalledExtensionsList(): Promise<void> {486const installedExtensions = await this.extensionManagementService.getInstalled();487this.installedExtensionIds = installedExtensions488.filter(ext => ext.manifest.contributes?.configuration)489.map(ext => ext.identifier.id);490}491492private restoreCachedState(): ISettingsEditor2State | null {493const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input);494if (cachedState && typeof cachedState.target === 'object') {495cachedState.target = URI.revive(cachedState.target);496}497498if (cachedState) {499const settingsTarget = cachedState.target;500this.settingsTargetsWidget.settingsTarget = settingsTarget;501this.viewState.settingsTarget = settingsTarget;502if (!this.searchWidget.getValue()) {503this.searchWidget.setValue(cachedState.searchQuery);504}505}506507if (this.input) {508this.editorMemento.clearEditorState(this.input, this.group);509}510511return cachedState ?? null;512}513514override getViewState(): object | undefined {515return this.viewState;516}517518override setOptions(options: ISettingsEditorOptions | undefined): void {519super.setOptions(options);520521if (options) {522this._setOptions(options);523}524}525526private _setOptions(options: ISettingsEditorOptions): void {527if (options.focusSearch && !platform.isIOS) {528// isIOS - #122044529this.focusSearch();530}531532const recoveredViewState = options.viewState ?533options.viewState as ISettingsEditorViewState : undefined;534535const query: string | undefined = recoveredViewState?.query ?? options.query;536if (query !== undefined) {537this.searchWidget.setValue(query);538this.viewState.query = query;539}540541const target: SettingsTarget | undefined = options.folderUri ?? recoveredViewState?.settingsTarget ?? <SettingsTarget | undefined>options.target;542if (target) {543this.settingsTargetsWidget.updateTarget(target);544}545}546547override clearInput(): void {548this.inSettingsEditorContextKey.set(false);549super.clearInput();550}551552layout(dimension: DOM.Dimension): void {553this.dimension = dimension;554555if (!this.isVisible()) {556return;557}558559this.layoutSplitView(dimension);560561const innerWidth = Math.min(this.headerContainer.clientWidth, dimension.width) - 24 * 2; // 24px padding on left and right;562// minus padding inside inputbox, countElement width, controls width, extra padding before countElement563const monacoWidth = innerWidth - 10 - this.countElement.clientWidth - this.controlsElement.clientWidth - 12;564this.searchWidget.layout(new DOM.Dimension(monacoWidth, 20));565566this.rootElement.classList.toggle('narrow-width', dimension.width < SettingsEditor2.NARROW_TOTAL_WIDTH);567}568569override focus(): void {570super.focus();571572if (this._currentFocusContext === SettingsFocusContext.Search) {573if (!platform.isIOS) {574// #122044575this.focusSearch();576}577} else if (this._currentFocusContext === SettingsFocusContext.SettingControl) {578const element = this.focusedSettingDOMElement;579if (element) {580const control = element.querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);581if (control) {582(<HTMLElement>control).focus();583return;584}585}586} else if (this._currentFocusContext === SettingsFocusContext.SettingTree) {587this.settingsTree.domFocus();588} else if (this._currentFocusContext === SettingsFocusContext.TableOfContents) {589this.tocTree.domFocus();590}591}592593protected override setEditorVisible(visible: boolean): void {594super.setEditorVisible(visible);595596if (!visible) {597// Wait for editor to be removed from DOM #106303598setTimeout(() => {599this.searchWidget.onHide();600this.settingRenderers.cancelSuggesters();601}, 0);602}603}604605focusSettings(focusSettingInput = false): void {606const focused = this.settingsTree.getFocus();607if (!focused.length) {608this.settingsTree.focusFirst();609}610611this.settingsTree.domFocus();612613if (focusSettingInput) {614const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`);615if (controlInFocusedRow) {616(<HTMLElement>controlInFocusedRow).focus();617}618}619}620621focusTOC(): void {622this.tocTree.domFocus();623}624625showContextMenu(): void {626const focused = this.settingsTree.getFocus()[0];627const rowElement = this.focusedSettingDOMElement;628if (rowElement && focused instanceof SettingsTreeSettingElement) {629this.settingRenderers.showContextMenu(focused, rowElement);630}631}632633focusSearch(filter?: string, selectAll = true): void {634if (filter && this.searchWidget) {635this.searchWidget.setValue(filter);636}637638// Do not select all if the user is already searching.639this.searchWidget.focus(selectAll && !this.searchInputDelayer.isTriggered);640}641642clearSearchResults(): void {643this.disableAiSearchToggle();644this.searchWidget.setValue('');645this.focusSearch();646}647648clearSearchFilters(): void {649const query = this.searchWidget.getValue();650651const splitQuery = query.split(' ').filter(word => {652return word.length && !SettingsEditor2.SUGGESTIONS.some(suggestion => word.startsWith(suggestion));653});654655this.searchWidget.setValue(splitQuery.join(' '));656}657658private updateInputAriaLabel() {659let label = searchBoxLabel;660if (this.searchResultLabel) {661label += `. ${this.searchResultLabel}`;662}663664if (this.lastSyncedLabel) {665label += `. ${this.lastSyncedLabel}`;666}667668this.searchWidget.updateAriaLabel(label);669}670671/**672* Render the header of the Settings editor, which includes the content above the splitview.673*/674private createHeader(parent: HTMLElement): void {675this.headerContainer = DOM.append(parent, $('.settings-header'));676this.searchContainer = DOM.append(this.headerContainer, $('.search-container'));677678const clearInputAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS,679localize('clearInput', "Clear Settings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false,680async () => this.clearSearchResults()681));682683const showAiResultActionClassNames = ['action-label', ThemeIcon.asClassName(preferencesAiResultsIcon)];684this.showAiResultsAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_SHOW_AI_RESULTS,685SHOW_AI_RESULTS_DISABLED_LABEL, showAiResultActionClassNames.join(' '), true686));687this._register(this.showAiResultsAction.onDidChange(async () => {688await this.onDidToggleAiSearch();689}));690691const filterAction = this._register(new Action(SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS,692localize('filterInput', "Filter Settings"), ThemeIcon.asClassName(preferencesFilterIcon)693));694695this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, this.searchContainer, {696triggerCharacters: ['@', ':'],697provideResults: (query: string) => {698// Based on testing, the trigger character is always at the end of the query.699// for the ':' trigger, only return suggestions if there was a '@' before it in the same word.700const queryParts = query.split(/\s/g);701if (queryParts[queryParts.length - 1].startsWith(`@${LANGUAGE_SETTING_TAG}`)) {702const sortedLanguages = this.languageService.getRegisteredLanguageIds().map(languageId => {703return `@${LANGUAGE_SETTING_TAG}${languageId} `;704}).sort();705return sortedLanguages.filter(langFilter => !query.includes(langFilter));706} else if (queryParts[queryParts.length - 1].startsWith(`@${EXTENSION_SETTING_TAG}`)) {707const installedExtensionsTags = this.installedExtensionIds.map(extensionId => {708return `@${EXTENSION_SETTING_TAG}${extensionId} `;709}).sort();710return installedExtensionsTags.filter(extFilter => !query.includes(extFilter));711} else if (queryParts[queryParts.length - 1].startsWith('@')) {712return SettingsEditor2.SUGGESTIONS.filter(tag => !query.includes(tag)).map(tag => tag.endsWith(':') ? tag : tag + ' ');713}714return [];715}716}, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, {717placeholderText: searchBoxLabel,718focusContextKey: this.searchFocusContextKey,719styleOverrides: {720inputBorder: settingsTextInputBorder721}722// TODO: Aria-live723}));724this._register(this.searchWidget.onDidFocus(() => {725this._currentFocusContext = SettingsFocusContext.Search;726}));727this._register(this.searchWidget.onInputDidChange(() => {728const searchVal = this.searchWidget.getValue();729clearInputAction.enabled = !!searchVal;730this.searchInputDelayer.trigger(() => this.onSearchInputChanged(true));731}));732733const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));734headerControlsContainer.style.borderColor = asCssVariable(settingsHeaderBorder);735736const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));737this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true }));738this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL;739this._register(this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target)));740this._register(DOM.addDisposableListener(targetWidgetContainer, DOM.EventType.KEY_DOWN, e => {741const event = new StandardKeyboardEvent(e);742if (event.keyCode === KeyCode.DownArrow) {743this.focusSettings();744}745}));746747if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) {748const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer));749this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => {750this.lastSyncedLabel = lastSyncedLabel;751this.updateInputAriaLabel();752}));753}754755this.controlsElement = DOM.append(this.searchContainer, DOM.$('.search-container-widgets'));756757this.countElement = DOM.append(this.controlsElement, DOM.$('.settings-count-widget.monaco-count-badge.long'));758this.countElement.style.backgroundColor = asCssVariable(badgeBackground);759this.countElement.style.color = asCssVariable(badgeForeground);760this.countElement.style.border = `1px solid ${asCssVariableWithDefault(contrastBorder, asCssVariable(inputBackground))}`;761762this.searchInputActionBar = this._register(new ActionBar(this.controlsElement, {763actionViewItemProvider: (action, options) => {764if (action.id === filterAction.id) {765return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, options, this.actionRunner, this.searchWidget);766}767if (this.showAiResultsAction && action.id === this.showAiResultsAction.id) {768const keybindingLabel = this.keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_TOGGLE_AI_SEARCH)?.getLabel();769return new ToggleActionViewItem(null, action, { ...options, keybinding: keybindingLabel, toggleStyles: defaultToggleStyles });770}771return undefined;772}773}));774775const actionsToPush = [clearInputAction, filterAction];776this.searchInputActionBar.push(actionsToPush, { label: false, icon: true });777778this.disableAiSearchToggle();779this.updateAiSearchToggleVisibility();780}781782toggleAiSearch(): void {783if (this.searchInputActionBar && this.showAiResultsAction && this.searchInputActionBar.hasAction(this.showAiResultsAction)) {784if (!this.showAiResultsAction.enabled) {785aria.status(localize('noAiResults', "No AI results available at this time."));786}787this.showAiResultsAction.checked = !this.showAiResultsAction.checked;788}789}790791private async onDidToggleAiSearch(): Promise<void> {792if (this.searchResultModel && this.showAiResultsAction) {793this.searchResultModel.showAiResults = this.showAiResultsAction.checked ?? false;794this.renderResultCountMessages(false);795this.onDidFinishSearch(true, undefined);796}797}798799private onDidSettingsTargetChange(target: SettingsTarget): void {800this.viewState.settingsTarget = target;801802// TODO Instead of rebuilding the whole model, refresh and uncache the inspected setting value803this.onConfigUpdate(undefined, true);804}805806private onDidDismissExtensionSetting(extensionId: string): void {807if (!this.dismissedExtensionSettings.includes(extensionId)) {808this.dismissedExtensionSettings.push(extensionId);809}810this.storageService.store(811this.DISMISSED_EXTENSION_SETTINGS_STORAGE_KEY,812this.dismissedExtensionSettings.join(this.DISMISSED_EXTENSION_SETTINGS_DELIMITER),813StorageScope.PROFILE,814StorageTarget.USER815);816this.onConfigUpdate(undefined, true);817}818819private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void {820const targetElement = this.currentSettingsModel?.getElementsByName(evt.targetKey)?.[0];821let revealFailed = false;822if (targetElement) {823let sourceTop = 0.5;824try {825const _sourceTop = this.settingsTree.getRelativeTop(evt.source);826if (_sourceTop !== null) {827sourceTop = _sourceTop;828}829} catch {830// e.g. clicked a searched element, now the search has been cleared831}832833// If we search for something and focus on a category, the settings tree834// only renders settings in that category.835// If the target display category is different than the source's, unfocus the category836// so that we can render all found settings again.837// Then, the reveal call will correctly find the target setting.838if (this.viewState.filterToCategory && evt.source.displayCategory !== targetElement.displayCategory) {839this.tocTree.setFocus([]);840}841try {842this.settingsTree.reveal(targetElement, sourceTop);843} catch (_) {844// The listwidget couldn't find the setting to reveal,845// even though it's in the model, meaning there might be a filter846// preventing it from showing up.847revealFailed = true;848}849850if (!revealFailed) {851// We need to shift focus from the setting that contains the link to the setting that's852// linked. Clicking on the link sets focus on the setting that contains the link,853// which is why we need the setTimeout.854setTimeout(() => {855this.settingsTree.setFocus([targetElement]);856}, 50);857858const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey);859if (domElements && domElements[0]) {860const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);861if (control) {862(<HTMLElement>control).focus();863}864}865}866}867868if (!recursed && (!targetElement || revealFailed)) {869// We'll call this event handler again after clearing the search query,870// so that more settings show up in the list.871const p = this.triggerSearch('', true);872p.then(() => {873this.searchWidget.setValue('');874this.onDidClickSetting(evt, true);875});876}877}878879switchToSettingsFile(): Promise<IEditorPane | undefined> {880const query = parseQuery(this.searchWidget.getValue()).query;881return this.openSettingsFile({ query });882}883884private async openSettingsFile(options?: ISettingsEditorOptions): Promise<IEditorPane | undefined> {885const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget;886887const openOptions: IOpenSettingsOptions = { jsonEditor: true, groupId: this.group.id, ...options };888if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) {889if (options?.revealSetting) {890const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();891const configurationScope = configurationProperties[options?.revealSetting.key]?.scope;892if (configurationScope && APPLICATION_SCOPES.includes(configurationScope)) {893return this.preferencesService.openApplicationSettings(openOptions);894}895}896return this.preferencesService.openUserSettings(openOptions);897} else if (currentSettingsTarget === ConfigurationTarget.USER_REMOTE) {898return this.preferencesService.openRemoteSettings(openOptions);899} else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) {900return this.preferencesService.openWorkspaceSettings(openOptions);901} else if (URI.isUri(currentSettingsTarget)) {902return this.preferencesService.openFolderSettings({ folderUri: currentSettingsTarget, ...openOptions });903}904905return undefined;906}907908private createBody(parent: HTMLElement): void {909this.bodyContainer = DOM.append(parent, $('.settings-body'));910911this.noResultsMessage = DOM.append(this.bodyContainer, $('.no-results-message'));912913this.noResultsMessage.innerText = localize('noResults', "No Settings Found");914915this.clearFilterLinkContainer = $('span.clear-search-filters');916917this.clearFilterLinkContainer.textContent = ' - ';918const clearFilterLink = DOM.append(this.clearFilterLinkContainer, $('a.pointer.prominent', { tabindex: 0 }, localize('clearSearchFilters', 'Clear Filters')));919this._register(DOM.addDisposableListener(clearFilterLink, DOM.EventType.CLICK, (e: MouseEvent) => {920DOM.EventHelper.stop(e, false);921this.clearSearchFilters();922}));923924DOM.append(this.noResultsMessage, this.clearFilterLinkContainer);925926this.noResultsMessage.style.color = asCssVariable(editorForeground);927928this.tocTreeContainer = $('.settings-toc-container');929this.settingsTreeContainer = $('.settings-tree-container');930931this.createTOC(this.tocTreeContainer);932this.createSettingsTree(this.settingsTreeContainer);933934this.splitView = this._register(new SplitView(this.bodyContainer, {935orientation: Orientation.HORIZONTAL,936proportionalLayout: true937}));938const startingWidth = this.storageService.getNumber('settingsEditor2.splitViewWidth', StorageScope.PROFILE, SettingsEditor2.TOC_RESET_WIDTH);939this.splitView.addView({940onDidChange: Event.None,941element: this.tocTreeContainer,942minimumSize: SettingsEditor2.TOC_MIN_WIDTH,943maximumSize: Number.POSITIVE_INFINITY,944layout: (width, _, height) => {945this.tocTreeContainer.style.width = `${width}px`;946this.tocTree.layout(height, width);947}948}, startingWidth, undefined, true);949this.splitView.addView({950onDidChange: Event.None,951element: this.settingsTreeContainer,952minimumSize: SettingsEditor2.EDITOR_MIN_WIDTH,953maximumSize: Number.POSITIVE_INFINITY,954layout: (width, _, height) => {955this.settingsTreeContainer.style.width = `${width}px`;956this.settingsTree.layout(height, width);957}958}, Sizing.Distribute, undefined, true);959this._register(this.splitView.onDidSashReset(() => {960const totalSize = this.splitView.getViewSize(0) + this.splitView.getViewSize(1);961this.splitView.resizeView(0, SettingsEditor2.TOC_RESET_WIDTH);962this.splitView.resizeView(1, totalSize - SettingsEditor2.TOC_RESET_WIDTH);963}));964this._register(this.splitView.onDidSashChange(() => {965const width = this.splitView.getViewSize(0);966this.storageService.store('settingsEditor2.splitViewWidth', width, StorageScope.PROFILE, StorageTarget.USER);967}));968const borderColor = this.theme.getColor(settingsSashBorder)!;969this.splitView.style({ separatorBorder: borderColor });970}971972private addCtrlAInterceptor(container: HTMLElement): void {973this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {974if (975e.keyCode === KeyCode.KeyA &&976(platform.isMacintosh ? e.metaKey : e.ctrlKey) &&977!DOM.isEditableElement(e.target)978) {979// Avoid browser ctrl+a980e.browserEvent.stopPropagation();981e.browserEvent.preventDefault();982}983}));984}985986private createTOC(container: HTMLElement): void {987this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState);988989this.tocTree = this._register(this.instantiationService.createInstance(TOCTree,990DOM.append(container, $('.settings-toc-wrapper', {991'role': 'navigation',992'aria-label': localize('settings', "Settings"),993})),994this.viewState));995this.tocTreeDisposed = false;996997this._register(this.tocTree.onDidFocus(() => {998this._currentFocusContext = SettingsFocusContext.TableOfContents;999}));10001001this._register(this.tocTree.onDidChangeFocus(e => {1002const element: SettingsTreeGroupElement | null = e.elements?.[0] ?? null;1003if (this.tocFocusedElement === element) {1004return;1005}10061007this.tocFocusedElement = element;1008this.tocTree.setSelection(element ? [element] : []);1009if (this.searchResultModel) {1010if (this.viewState.filterToCategory !== element) {1011this.viewState.filterToCategory = element ?? undefined;1012// Force render in this case, because1013// onDidClickSetting relies on the updated view.1014this.renderTree(undefined, true);1015this.settingsTree.scrollTop = 0;1016}1017} else if (element && (!e.browserEvent || !(<IFocusEventFromScroll>e.browserEvent).fromScroll)) {1018this.settingsTree.reveal(element, 0);1019this.settingsTree.setFocus([element]);1020}1021}));10221023this._register(this.tocTree.onDidFocus(() => {1024this.tocRowFocused.set(true);1025}));10261027this._register(this.tocTree.onDidBlur(() => {1028this.tocRowFocused.set(false);1029}));10301031this._register(this.tocTree.onDidDispose(() => {1032this.tocTreeDisposed = true;1033}));1034}10351036private applyFilter(filter: string) {1037if (this.searchWidget && !this.searchWidget.getValue().includes(filter)) {1038// Prepend the filter to the query.1039const newQuery = `${filter} ${this.searchWidget.getValue().trimStart()}`;1040this.focusSearch(newQuery, false);1041}1042}10431044private removeLanguageFilters() {1045if (this.searchWidget && this.searchWidget.getValue().includes(`@${LANGUAGE_SETTING_TAG}`)) {1046const query = this.searchWidget.getValue().split(' ');1047const newQuery = query.filter(word => !word.startsWith(`@${LANGUAGE_SETTING_TAG}`)).join(' ');1048this.focusSearch(newQuery, false);1049}1050}10511052private createSettingsTree(container: HTMLElement): void {1053this.settingRenderers = this._register(this.instantiationService.createInstance(SettingTreeRenderers));1054this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset, e.scope)));1055this._register(this.settingRenderers.onDidDismissExtensionSetting((e) => this.onDidDismissExtensionSetting(e)));1056this._register(this.settingRenderers.onDidOpenSettings(settingKey => {1057this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } });1058}));1059this._register(this.settingRenderers.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName)));1060this._register(this.settingRenderers.onDidFocusSetting(element => {1061this.settingsTree.setFocus([element]);1062this._currentFocusContext = SettingsFocusContext.SettingControl;1063this.settingRowFocused.set(false);1064}));1065this._register(this.settingRenderers.onDidChangeSettingHeight((params: HeightChangeParams) => {1066const { element, height } = params;1067try {1068this.settingsTree.updateElementHeight(element, height);1069} catch (e) {1070// the element was not found1071}1072}));1073this._register(this.settingRenderers.onApplyFilter((filter) => this.applyFilter(filter)));1074this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => {1075this.removeLanguageFilters();1076if (element.language) {1077this.applyFilter(`@${LANGUAGE_SETTING_TAG}${element.language}`);1078}10791080if (element.scope === 'workspace') {1081this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE);1082} else if (element.scope === 'user') {1083this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL);1084} else if (element.scope === 'remote') {1085this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE);1086}1087this.applyFilter(`@${ID_SETTING_TAG}${element.settingKey}`);1088}));10891090this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree,1091container,1092this.viewState,1093this.settingRenderers.allRenderers));10941095this._register(this.settingsTree.onDidScroll(() => {1096if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) {1097return;1098}10991100this.settingsTreeScrollTop = this.settingsTree.scrollTop;11011102// setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when1103// setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync1104setTimeout(() => {1105this.updateTreeScrollSync();1106}, 0);1107}));11081109this._register(this.settingsTree.onDidFocus(() => {1110const classList = container.ownerDocument.activeElement?.classList;1111if (classList && classList.contains('monaco-list') && classList.contains('settings-editor-tree')) {1112this._currentFocusContext = SettingsFocusContext.SettingTree;1113this.settingRowFocused.set(true);1114this.treeFocusedElement ??= this.settingsTree.firstVisibleElement ?? null;1115if (this.treeFocusedElement) {1116this.treeFocusedElement.tabbable = true;1117}1118}1119}));11201121this._register(this.settingsTree.onDidBlur(() => {1122this.settingRowFocused.set(false);1123// Clear out the focused element, otherwise it could be1124// out of date during the next onDidFocus event.1125this.treeFocusedElement = null;1126}));11271128// There is no different select state in the settings tree1129this._register(this.settingsTree.onDidChangeFocus(e => {1130const element = e.elements[0];1131if (this.treeFocusedElement === element) {1132return;1133}11341135if (this.treeFocusedElement) {1136this.treeFocusedElement.tabbable = false;1137}11381139this.treeFocusedElement = element;11401141if (this.treeFocusedElement) {1142this.treeFocusedElement.tabbable = true;1143}11441145this.settingsTree.setSelection(element ? [element] : []);1146}));1147}11481149private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[], manualReset: boolean, scope: ConfigurationScope | undefined): void {1150const parsedQuery = parseQuery(this.searchWidget.getValue());1151const languageFilter = parsedQuery.languageFilter;1152if (manualReset || (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key)) {1153this.updateChangedSetting(key, value, manualReset, languageFilter, scope);1154}11551156this.pendingSettingUpdate = { key, value, languageFilter };1157if (SettingsEditor2.shouldSettingUpdateFast(type)) {1158this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope));1159} else {1160this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter, scope));1161}1162}11631164private updateTreeScrollSync(): void {1165this.settingRenderers.cancelSuggesters();1166if (this.searchResultModel) {1167return;1168}11691170if (!this.tocTreeModel) {1171return;1172}11731174const elementToSync = this.settingsTree.firstVisibleElement;1175const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent :1176elementToSync instanceof SettingsTreeGroupElement ? elementToSync :1177null;11781179// 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 because1180// it is focused. So, bail if element doesn't exist in the TOC.1181let nodeExists = true;1182try { this.tocTree.getNode(element); } catch (e) { nodeExists = false; }1183if (!nodeExists) {1184return;1185}11861187if (element && this.tocTree.getSelection()[0] !== element) {1188const ancestors = this.getAncestors(element);1189ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));11901191this.tocTree.reveal(element);1192const elementTop = this.tocTree.getRelativeTop(element);1193if (typeof elementTop !== 'number') {1194return;1195}11961197this.tocTree.collapseAll();11981199ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));1200if (elementTop < 0 || elementTop > 1) {1201this.tocTree.reveal(element);1202} else {1203this.tocTree.reveal(element, elementTop);1204}12051206this.tocTree.expand(element);12071208this.tocTree.setSelection([element]);12091210const fakeKeyboardEvent = new KeyboardEvent('keydown');1211(<IFocusEventFromScroll>fakeKeyboardEvent).fromScroll = true;1212this.tocTree.setFocus([element], fakeKeyboardEvent);1213}1214}12151216private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] {1217const ancestors: any[] = [];12181219while (element.parent) {1220if (element.parent.id !== 'root') {1221ancestors.push(element.parent);1222}12231224element = element.parent;1225}12261227return ancestors.reverse();1228}12291230private updateChangedSetting(key: string, value: any, manualReset: boolean, languageFilter: string | undefined, scope: ConfigurationScope | undefined): Promise<void> {1231// ConfigurationService displays the error if this fails.1232// Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change.1233const settingsTarget = this.settingsTargetsWidget.settingsTarget;1234const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined;1235const configurationTarget = <ConfigurationTarget | null>(resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget) ?? ConfigurationTarget.USER_LOCAL;1236const overrides: IConfigurationUpdateOverrides = { resource, overrideIdentifiers: languageFilter ? [languageFilter] : undefined };12371238const configurationTargetIsWorkspace = configurationTarget === ConfigurationTarget.WORKSPACE || configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER;12391240const userPassedInManualReset = configurationTargetIsWorkspace || !!languageFilter;1241const isManualReset = userPassedInManualReset ? manualReset : value === undefined;12421243// If the user is changing the value back to the default, and we're not targeting a workspace scope, do a 'reset' instead1244const inspected = this.configurationService.inspect(key, overrides);1245if (!userPassedInManualReset && inspected.defaultValue === value) {1246value = undefined;1247}12481249return this.configurationService.updateValue(key, value, overrides, configurationTarget, { handleDirtyFile: 'save' })1250.then(() => {1251const query = this.searchWidget.getValue();1252if (query.includes(`@${MODIFIED_SETTING_TAG}`)) {1253// The user might have reset a setting.1254this.refreshTOCTree();1255}1256this.renderTree(key, isManualReset);1257this.pendingSettingUpdate = null;12581259const reportModifiedProps = {1260key,1261query,1262searchResults: this.searchResultModel?.getUniqueSearchResults() ?? null,1263rawResults: this.searchResultModel?.getRawResults() ?? null,1264showConfiguredOnly: !!this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG),1265isReset: typeof value === 'undefined',1266settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget1267};1268return this.reportModifiedSetting(reportModifiedProps);1269});1270}12711272private reportModifiedSetting(props: { key: string; query: string; searchResults: ISearchResult | null; rawResults: ISearchResult[] | null; showConfiguredOnly: boolean; isReset: boolean; settingsTarget: SettingsTarget }): void {1273type SettingsEditorModifiedSettingEvent = {1274key: string;1275groupId: string | undefined;1276providerName: string | undefined;1277nlpIndex: number | undefined;1278displayIndex: number | undefined;1279showConfiguredOnly: boolean;1280isReset: boolean;1281target: string;1282};1283type SettingsEditorModifiedSettingClassification = {1284key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The setting that is being modified.' };1285groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' };1286providerName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the search provider, if applicable.' };1287nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The index of the setting in the remote search provider results, if applicable.' };1288displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The index of the setting in the combined search results, if applicable.' };1289showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is in the modified view, which shows configured settings only.' };1290isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Identifies whether a setting was reset to its default value.' };1291target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The scope of the setting, such as user or workspace.' };1292owner: 'rzhao271';1293comment: 'Event emitted when the user modifies a setting in the Settings editor';1294};12951296let groupId: string | undefined = undefined;1297let providerName: string | undefined = undefined;1298let nlpIndex: number | undefined = undefined;1299let displayIndex: number | undefined = undefined;1300if (props.searchResults) {1301displayIndex = props.searchResults.filterMatches.findIndex(m => m.setting.key === props.key);13021303if (this.searchResultModel) {1304providerName = props.searchResults.filterMatches.find(m => m.setting.key === props.key)?.providerName;1305const rawResults = this.searchResultModel.getRawResults();1306if (rawResults[SearchResultIdx.Local] && displayIndex >= 0) {1307const settingInLocalResults = rawResults[SearchResultIdx.Local].filterMatches.some(m => m.setting.key === props.key);1308groupId = settingInLocalResults ? 'local' : 'remote';1309}1310if (rawResults[SearchResultIdx.Remote]) {1311const _nlpIndex = rawResults[SearchResultIdx.Remote].filterMatches.findIndex(m => m.setting.key === props.key);1312nlpIndex = _nlpIndex >= 0 ? _nlpIndex : undefined;1313}1314}1315}13161317const reportedTarget = props.settingsTarget === ConfigurationTarget.USER_LOCAL ? 'user' :1318props.settingsTarget === ConfigurationTarget.USER_REMOTE ? 'user_remote' :1319props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' :1320'folder';13211322const data = {1323key: props.key,1324groupId,1325providerName,1326nlpIndex,1327displayIndex,1328showConfiguredOnly: props.showConfiguredOnly,1329isReset: props.isReset,1330target: reportedTarget1331};13321333this.telemetryService.publicLog2<SettingsEditorModifiedSettingEvent, SettingsEditorModifiedSettingClassification>('settingsEditor.settingModified', data);1334}13351336private scheduleRefresh(element: HTMLElement, key = ''): void {1337if (key && this.scheduledRefreshes.has(key)) {1338return;1339}13401341if (!key) {1342dispose(this.scheduledRefreshes.values());1343this.scheduledRefreshes.clear();1344}13451346const store = new DisposableStore();1347const scheduledRefreshTracker = DOM.trackFocus(element);1348store.add(scheduledRefreshTracker);1349store.add(scheduledRefreshTracker.onDidBlur(() => {1350this.scheduledRefreshes.get(key)?.dispose();1351this.scheduledRefreshes.delete(key);1352this.onConfigUpdate(new Set([key]));1353}));1354this.scheduledRefreshes.set(key, store);1355}13561357private createSettingsOrderByTocIndex(resolvedSettingsRoot: ITOCEntry<ISetting>): Map<string, number> {1358const index = new Map<string, number>();1359function indexSettings(resolvedSettingsRoot: ITOCEntry<ISetting>, counter = 0): number {1360if (resolvedSettingsRoot.settings) {1361for (const setting of resolvedSettingsRoot.settings) {1362if (!index.has(setting.key)) {1363index.set(setting.key, counter++);1364}1365}1366}1367if (resolvedSettingsRoot.children) {1368for (const child of resolvedSettingsRoot.children) {1369counter = indexSettings(child, counter);1370}1371}1372return counter;1373}1374indexSettings(resolvedSettingsRoot);1375return index;1376}13771378private refreshModels(resolvedSettingsRoot: ITOCEntry<ISetting>) {1379// Both calls to refreshModels require a valid settingsTreeModel.1380this.settingsTreeModel.value!.update(resolvedSettingsRoot);1381this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.value!.root;1382this.settingsOrderByTocIndex = this.createSettingsOrderByTocIndex(resolvedSettingsRoot);1383}13841385private async onConfigUpdate(keys?: ReadonlySet<string>, forceRefresh = false, schemaChange = false): Promise<void> {1386if (keys && this.settingsTreeModel) {1387return this.updateElementsByKey(keys);1388}13891390if (!this.defaultSettingsEditorModel) {1391return;1392}13931394const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed13951396const coreSettings = groups.filter(g => !g.extensionInfo);1397const settingsResult = resolveSettingsTree(tocData, coreSettings, this.logService);1398const resolvedSettingsRoot = settingsResult.tree;13991400// Warn for settings not included in layout1401if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) {1402const settingKeyList: string[] = [];1403settingsResult.leftoverSettings.forEach(s => {1404settingKeyList.push(s.key);1405});14061407this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`);1408this.hasWarnedMissingSettings = true;1409}14101411const additionalGroups: ISettingsGroup[] = [];1412let setAdditionalGroups = false;1413const toggleData = await getExperimentalExtensionToggleData(this.contextKeyService, this.extensionGalleryService, this.productService);1414if (toggleData && groups.filter(g => g.extensionInfo).length) {1415for (const key in toggleData.settingsEditorRecommendedExtensions) {1416const extension: IGalleryExtension = toggleData.recommendedExtensionsGalleryInfo[key];1417if (!extension) {1418continue;1419}14201421const extensionId = extension.identifier.id;1422// prevent race between extension update handler and this (onConfigUpdate) handler1423await this.refreshInstalledExtensionsList();1424const extensionInstalled = this.installedExtensionIds.includes(extensionId);14251426// Drill down to see whether the group and setting already exist1427// and need to be removed.1428const matchingGroupIndex = groups.findIndex(g =>1429g.extensionInfo && g.extensionInfo!.id.toLowerCase() === extensionId.toLowerCase() &&1430g.sections.length === 1 && g.sections[0].settings.length === 1 && g.sections[0].settings[0].displayExtensionId1431);1432if (extensionInstalled || this.dismissedExtensionSettings.includes(extensionId)) {1433if (matchingGroupIndex !== -1) {1434groups.splice(matchingGroupIndex, 1);1435setAdditionalGroups = true;1436}1437continue;1438}14391440if (matchingGroupIndex !== -1) {1441continue;1442}14431444// Create the entry. extensionInstalled is false in this case.1445let manifest: IExtensionManifest | null = null;1446try {1447manifest = await raceTimeout(1448this.extensionGalleryService.getManifest(extension, CancellationToken.None),1449EXTENSION_FETCH_TIMEOUT_MS1450) ?? null;1451} catch (e) {1452// Likely a networking issue.1453// Skip adding a button for this extension to the Settings editor.1454continue;1455}14561457if (manifest === null) {1458continue;1459}14601461const contributesConfiguration = manifest?.contributes?.configuration;14621463let groupTitle: string | undefined;1464if (!Array.isArray(contributesConfiguration)) {1465groupTitle = contributesConfiguration?.title;1466} else if (contributesConfiguration.length === 1) {1467groupTitle = contributesConfiguration[0].title;1468}14691470const recommendationInfo = toggleData.settingsEditorRecommendedExtensions[key];1471const extensionName = extension.displayName ?? extension.name ?? extensionId;1472const settingKey = `${key}.manageExtension`;1473const setting: ISetting = {1474range: nullRange,1475key: settingKey,1476keyRange: nullRange,1477value: null,1478valueRange: nullRange,1479description: [recommendationInfo.onSettingsEditorOpen?.descriptionOverride ?? extension.description],1480descriptionIsMarkdown: false,1481descriptionRanges: [],1482scope: ConfigurationScope.WINDOW,1483type: 'null',1484displayExtensionId: extensionId,1485extensionGroupTitle: groupTitle ?? extensionName,1486categoryLabel: 'Extensions',1487title: extensionName1488};1489const additionalGroup: ISettingsGroup = {1490sections: [{1491settings: [setting],1492}],1493id: extensionId,1494title: setting.extensionGroupTitle!,1495titleRange: nullRange,1496range: nullRange,1497extensionInfo: {1498id: extensionId,1499displayName: extension.displayName,1500}1501};1502groups.push(additionalGroup);1503additionalGroups.push(additionalGroup);1504setAdditionalGroups = true;1505}1506}15071508resolvedSettingsRoot.children!.push(await createTocTreeForExtensionSettings(this.extensionService, groups.filter(g => g.extensionInfo)));15091510const commonlyUsedDataToUse = getCommonlyUsedData(toggleData);1511const commonlyUsed = resolveSettingsTree(commonlyUsedDataToUse, groups, this.logService);1512resolvedSettingsRoot.children!.unshift(commonlyUsed.tree);15131514if (toggleData && setAdditionalGroups) {1515// Add the additional groups to the model to help with searching.1516this.defaultSettingsEditorModel.setAdditionalGroups(additionalGroups);1517}15181519if (!this.workspaceTrustManagementService.isWorkspaceTrusted() && (this.viewState.settingsTarget instanceof URI || this.viewState.settingsTarget === ConfigurationTarget.WORKSPACE)) {1520const configuredUntrustedWorkspaceSettings = resolveConfiguredUntrustedSettings(groups, this.viewState.settingsTarget, this.viewState.languageFilter, this.configurationService);1521if (configuredUntrustedWorkspaceSettings.length) {1522resolvedSettingsRoot.children!.unshift({1523id: 'workspaceTrust',1524label: localize('settings require trust', "Workspace Trust"),1525settings: configuredUntrustedWorkspaceSettings1526});1527}1528}15291530this.searchResultModel?.updateChildren();15311532if (this.settingsTreeModel.value) {1533this.refreshModels(resolvedSettingsRoot);15341535if (schemaChange && this.searchResultModel) {1536// If an extension's settings were just loaded and a search is active, retrigger the search so it shows up1537return await this.onSearchInputChanged(false);1538}15391540this.refreshTOCTree();1541this.renderTree(undefined, forceRefresh);1542} else {1543this.settingsTreeModel.value = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());1544this.refreshModels(resolvedSettingsRoot);15451546// Don't restore the cached state if we already have a query value from calling _setOptions().1547const cachedState = !this.viewState.query ? this.restoreCachedState() : undefined;1548if (cachedState?.searchQuery || this.searchWidget.getValue()) {1549await this.onSearchInputChanged(true);1550} else {1551this.refreshTOCTree();1552this.refreshTree();1553this.tocTree.collapseAll();1554}1555}1556}15571558private updateElementsByKey(keys: ReadonlySet<string>): void {1559if (keys.size) {1560if (this.searchResultModel) {1561keys.forEach(key => this.searchResultModel!.updateElementsByName(key));1562}15631564if (this.settingsTreeModel.value) {1565keys.forEach(key => this.settingsTreeModel.value!.updateElementsByName(key));1566}15671568keys.forEach(key => this.renderTree(key));1569} else {1570this.renderTree();1571}1572}15731574private getActiveControlInSettingsTree(): HTMLElement | null {1575const element = this.settingsTree.getHTMLElement();1576const activeElement = element.ownerDocument.activeElement;1577return (activeElement && DOM.isAncestorOfActiveElement(element)) ?1578<HTMLElement>activeElement :1579null;1580}15811582private renderTree(key?: string, force = false): void {1583if (!force && key && this.scheduledRefreshes.has(key)) {1584this.updateModifiedLabelForKey(key);1585return;1586}15871588// If the context view is focused, delay rendering settings1589if (this.contextViewFocused()) {1590const element = this.window.document.querySelector('.context-view');1591if (element) {1592this.scheduleRefresh(element as HTMLElement, key);1593}1594return;1595}15961597// If a setting control is currently focused, schedule a refresh for later1598const activeElement = this.getActiveControlInSettingsTree();1599const focusedSetting = activeElement && this.settingRenderers.getSettingDOMElementForDOMElement(activeElement);1600if (focusedSetting && !force) {1601// If a single setting is being refreshed, it's ok to refresh now if that is not the focused setting1602if (key) {1603const focusedKey = focusedSetting.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR);1604if (focusedKey === key &&1605// update `list`s live, as they have a separate "submit edit" step built in before this1606(focusedSetting.parentElement && !focusedSetting.parentElement.classList.contains('setting-item-list'))1607) {1608this.updateModifiedLabelForKey(key);1609this.scheduleRefresh(focusedSetting, key);1610return;1611}1612} else {1613this.scheduleRefresh(focusedSetting);1614return;1615}1616}16171618this.renderResultCountMessages(false);16191620if (key) {1621const elements = this.currentSettingsModel?.getElementsByName(key);1622if (elements?.length) {1623if (elements.length >= 2) {1624console.warn('More than one setting with key ' + key + ' found');1625}1626this.refreshSingleElement(elements[0]);1627} else {1628// Refresh requested for a key that we don't know about1629return;1630}1631} else {1632this.refreshTree();1633}16341635return;1636}16371638private contextViewFocused(): boolean {1639return !!DOM.findParentWithClass(<HTMLElement>this.rootElement.ownerDocument.activeElement, 'context-view');1640}16411642private refreshSingleElement(element: SettingsTreeSettingElement): void {1643if (this.isVisible()1644&& this.settingsTree.hasElement(element)1645&& (!element.setting.deprecationMessage || element.isConfigured)) {1646this.settingsTree.rerender(element);1647}1648}16491650private refreshTree(): void {1651if (this.isVisible() && this.currentSettingsModel) {1652this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root));1653}1654}16551656private refreshTOCTree(): void {1657if (this.isVisible()) {1658this.tocTreeModel.update();1659this.tocTree.setChildren(null, createTOCIterator(this.tocTreeModel, this.tocTree));1660}1661}16621663private updateModifiedLabelForKey(key: string): void {1664if (!this.currentSettingsModel) {1665return;1666}1667const dataElements = this.currentSettingsModel.getElementsByName(key);1668const isModified = dataElements && dataElements[0] && dataElements[0].isConfigured; // all elements are either configured or not1669const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key);1670if (elements && elements[0]) {1671elements[0].classList.toggle('is-configured', !!isModified);1672}1673}16741675private async onSearchInputChanged(expandResults: boolean): Promise<void> {1676if (!this.currentSettingsModel) {1677// Initializing search widget value1678return;1679}16801681const query = this.searchWidget.getValue().trim();1682this.viewState.query = query;1683await this.triggerSearch(query.replace(/\u203A/g, ' '), expandResults);1684}16851686private parseSettingFromJSON(query: string): string | null {1687const match = query.match(/"([a-zA-Z.]+)": /);1688return match && match[1];1689}16901691/**1692* Toggles the visibility of the Settings editor table of contents during a search1693* depending on the behavior.1694*/1695private toggleTocBySearchBehaviorType() {1696const tocBehavior = this.configurationService.getValue<'filter' | 'hide'>(SEARCH_TOC_BEHAVIOR_KEY);1697const hideToc = tocBehavior === 'hide';1698if (hideToc) {1699this.splitView.setViewVisible(0, false);1700this.splitView.style({1701separatorBorder: Color.transparent1702});1703} else {1704this.layoutSplitView(this.dimension);1705}1706}17071708private async triggerSearch(query: string, expandResults: boolean): Promise<void> {1709const progressRunner = this.editorProgressService.show(true, 800);1710this.viewState.tagFilters = new Set<string>();1711this.viewState.extensionFilters = new Set<string>();1712this.viewState.featureFilters = new Set<string>();1713this.viewState.idFilters = new Set<string>();1714this.viewState.languageFilter = undefined;1715if (query) {1716const parsedQuery = parseQuery(query);1717query = parsedQuery.query;1718parsedQuery.tags.forEach(tag => this.viewState.tagFilters!.add(tag));1719parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId));1720parsedQuery.featureFilters.forEach(feature => this.viewState.featureFilters!.add(feature));1721parsedQuery.idFilters.forEach(id => this.viewState.idFilters!.add(id));1722this.viewState.languageFilter = parsedQuery.languageFilter;1723}17241725this.settingsTargetsWidget.updateLanguageFilterIndicators(this.viewState.languageFilter);17261727if (query && query !== '@') {1728query = this.parseSettingFromJSON(query) || query;1729await this.triggerFilterPreferences(query, expandResults, progressRunner);1730this.toggleTocBySearchBehaviorType();1731} else {1732if (this.viewState.tagFilters.size || this.viewState.extensionFilters.size || this.viewState.featureFilters.size || this.viewState.idFilters.size || this.viewState.languageFilter) {1733this.searchResultModel = this.createFilterModel();1734} else {1735this.searchResultModel = null;1736}17371738this.searchDelayer.cancel();1739if (this.searchInProgress) {1740this.searchInProgress.dispose(true);1741this.searchInProgress = null;1742}17431744if (expandResults) {1745this.tocTree.setFocus([]);1746this.viewState.filterToCategory = undefined;1747}1748this.tocTreeModel.currentSearchModel = this.searchResultModel;17491750if (this.searchResultModel) {1751// Added a filter model1752if (expandResults) {1753this.tocTree.setSelection([]);1754this.tocTree.expandAll();1755}1756this.refreshTOCTree();1757this.renderResultCountMessages(false);1758this.refreshTree();1759this.toggleTocBySearchBehaviorType();1760} else if (!this.tocTreeDisposed) {1761// Leaving search mode1762this.tocTree.collapseAll();1763this.refreshTOCTree();1764this.renderResultCountMessages(false);1765this.refreshTree();1766this.layoutSplitView(this.dimension);1767}1768progressRunner.done();1769}1770}17711772/**1773* Return a fake SearchResultModel which can hold a flat list of all settings, to be filtered (@modified etc)1774*/1775private createFilterModel(): SearchResultModel {1776const filterModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted());17771778const fullResult: ISearchResult = {1779filterMatches: [],1780exactMatch: false,1781};1782for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) {1783for (const sect of g.sections) {1784for (const setting of sect.settings) {1785fullResult.filterMatches.push({1786setting,1787matches: [],1788matchType: SettingMatchType.None,1789keyMatchScore: 0,1790score: 0,1791providerName: FILTER_MODEL_SEARCH_PROVIDER_NAME1792});1793}1794}1795}17961797filterModel.setResult(0, fullResult);1798return filterModel;1799}18001801private async triggerFilterPreferences(query: string, expandResults: boolean, progressRunner: IProgressRunner): Promise<void> {1802if (this.searchInProgress) {1803this.searchInProgress.dispose(true);1804this.searchInProgress = null;1805}18061807const searchInProgress = this.searchInProgress = new CancellationTokenSource();1808return this.searchDelayer.trigger(async () => {1809if (searchInProgress.token.isCancellationRequested) {1810return;1811}1812this.disableAiSearchToggle();1813const localResults = await this.doLocalSearch(query, searchInProgress.token);1814if (!this.searchResultModel || searchInProgress.token.isCancellationRequested) {1815return;1816}1817this.searchResultModel.showAiResults = false;18181819if (localResults && localResults.filterMatches.length > 0) {1820// The remote results might take a while and1821// are always appended to the end anyway, so1822// show some results now.1823this.onDidFinishSearch(expandResults, undefined);1824}18251826if (!localResults || !localResults.exactMatch) {1827await this.doRemoteSearch(query, searchInProgress.token);1828}1829if (searchInProgress.token.isCancellationRequested) {1830return;1831}18321833if (this.aiSearchPromise) {1834this.aiSearchPromise.cancel();1835}18361837// Kick off an AI search in the background if the toggle is shown.1838// We purposely do not await it.1839if (this.searchInputActionBar && this.showAiResultsAction && this.searchInputActionBar.hasAction(this.showAiResultsAction)) {1840this.aiSearchPromise = createCancelablePromise(token => {1841return this.doAiSearch(query, token).then((results) => {1842if (results && this.showAiResultsAction) {1843this.showAiResultsAction.enabled = true;1844this.aiResultsAvailable.set(true);1845this.showAiResultsAction.label = SHOW_AI_RESULTS_ENABLED_LABEL;1846this.renderResultCountMessages(true);1847}1848}).catch(e => {1849if (!isCancellationError(e)) {1850this.logService.trace('Error during AI settings search:', e);1851}1852});1853});1854}18551856this.onDidFinishSearch(expandResults, progressRunner);1857});1858}18591860private onDidFinishSearch(expandResults: boolean, progressRunner: IProgressRunner | undefined): void {1861this.tocTreeModel.currentSearchModel = this.searchResultModel;1862if (expandResults) {1863this.tocTree.setFocus([]);1864this.viewState.filterToCategory = undefined;1865this.tocTree.expandAll();1866this.settingsTree.scrollTop = 0;1867}1868this.refreshTOCTree();1869this.renderTree(undefined, true);1870progressRunner?.done();1871}18721873private doLocalSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {1874const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);1875return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, STRING_MATCH_SEARCH_PROVIDER_NAME, token);1876}18771878private doRemoteSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {1879const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);1880if (!remoteSearchProvider) {1881return Promise.resolve(null);1882}1883return this.searchWithProvider(SearchResultIdx.Remote, remoteSearchProvider, TF_IDF_SEARCH_PROVIDER_NAME, token);1884}18851886private async doAiSearch(query: string, token: CancellationToken): Promise<ISearchResult | null> {1887const aiSearchProvider = this.preferencesSearchService.getAiSearchProvider(query);1888if (!aiSearchProvider) {1889return null;1890}18911892const embeddingsResults = await this.searchWithProvider(SearchResultIdx.Embeddings, aiSearchProvider, EMBEDDINGS_SEARCH_PROVIDER_NAME, token);1893if (!embeddingsResults || token.isCancellationRequested) {1894return null;1895}18961897const llmResults = await this.getLLMRankedResults(query, token);1898if (token.isCancellationRequested) {1899return null;1900}19011902return {1903filterMatches: embeddingsResults.filterMatches.concat(llmResults?.filterMatches ?? []),1904exactMatch: false1905};1906}19071908private async getLLMRankedResults(query: string, token: CancellationToken): Promise<ISearchResult | null> {1909const aiSearchProvider = this.preferencesSearchService.getAiSearchProvider(query);1910if (!aiSearchProvider) {1911return null;1912}19131914this.stopWatch.reset();1915const result = await aiSearchProvider.getLLMRankedResults(token);1916this.stopWatch.stop();19171918if (token.isCancellationRequested) {1919return null;1920}19211922// Only log the elapsed time if there are actual results.1923if (result && result.filterMatches.length > 0) {1924const elapsed = this.stopWatch.elapsed();1925this.logSearchPerformance(LLM_RANKED_SEARCH_PROVIDER_NAME, elapsed);1926}19271928this.searchResultModel!.setResult(SearchResultIdx.AiSelected, result);1929return result;1930}19311932private async searchWithProvider(type: SearchResultIdx, searchProvider: ISearchProvider, providerName: string, token: CancellationToken): Promise<ISearchResult | null> {1933this.stopWatch.reset();1934const result = await this._searchPreferencesModel(this.defaultSettingsEditorModel, searchProvider, token);1935this.stopWatch.stop();19361937if (token.isCancellationRequested) {1938// Handle cancellation like this because cancellation is lost inside the search provider due to async/await1939return null;1940}19411942// Only log the elapsed time if there are actual results.1943if (result && result.filterMatches.length > 0) {1944const elapsed = this.stopWatch.elapsed();1945this.logSearchPerformance(providerName, elapsed);1946}19471948this.searchResultModel ??= this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted());1949this.searchResultModel.setResult(type, result);1950return result;1951}19521953private logSearchPerformance(providerName: string, elapsed: number): void {1954type SettingsEditorSearchPerformanceEvent = {1955providerName: string | undefined;1956elapsedMs: number;1957};1958type SettingsEditorSearchPerformanceClassification = {1959providerName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the search provider, if applicable.' };1960elapsedMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The time taken to perform the search, in milliseconds.' };1961owner: 'rzhao271';1962comment: 'Event emitted when the Settings editor calls a search provider to search for a setting';1963};1964this.telemetryService.publicLog2<SettingsEditorSearchPerformanceEvent, SettingsEditorSearchPerformanceClassification>('settingsEditor.searchPerformance', {1965providerName,1966elapsedMs: elapsed,1967});1968}19691970private renderResultCountMessages(showAiResultsMessage: boolean) {1971if (!this.currentSettingsModel) {1972return;1973}19741975this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 01976? 'initial'1977: 'none';19781979if (!this.searchResultModel) {1980if (this.countElement.style.display !== 'none') {1981this.searchResultLabel = null;1982this.updateInputAriaLabel();1983this.countElement.style.display = 'none';1984this.countElement.innerText = '';1985this.layout(this.dimension);1986}19871988this.rootElement.classList.remove('no-results');1989this.splitView.el.style.visibility = 'visible';1990return;1991} else {1992const count = this.searchResultModel.getUniqueResultsCount();1993let resultString: string;19941995if (showAiResultsMessage) {1996switch (count) {1997case 0: resultString = localize('noResultsWithAiAvailable', "No Settings Found. AI Results Available"); break;1998case 1: resultString = localize('oneResultWithAiAvailable', "1 Setting Found. AI Results Available"); break;1999default: resultString = localize('moreThanOneResultWithAiAvailable', "{0} Settings Found. AI Results Available", count);2000}2001} else {2002switch (count) {2003case 0: resultString = localize('noResults', "No Settings Found"); break;2004case 1: resultString = localize('oneResult', "1 Setting Found"); break;2005default: resultString = localize('moreThanOneResult', "{0} Settings Found", count);2006}2007}20082009this.searchResultLabel = resultString;2010this.updateInputAriaLabel();2011this.countElement.innerText = resultString;2012aria.status(resultString);20132014if (this.countElement.style.display !== 'block') {2015this.countElement.style.display = 'block';2016this.layout(this.dimension);2017}2018this.rootElement.classList.toggle('no-results', count === 0);2019this.splitView.el.style.visibility = count === 0 ? 'hidden' : 'visible';2020}2021}20222023private async _searchPreferencesModel(model: ISettingsEditorModel, provider: ISearchProvider, token: CancellationToken): Promise<ISearchResult | null> {2024try {2025return await provider.searchModel(model, token);2026} catch (err) {2027if (isCancellationError(err)) {2028return Promise.reject(err);2029} else {2030return null;2031}2032}2033}20342035private layoutSplitView(dimension: DOM.Dimension): void {2036if (!this.isVisible()) {2037return;2038}2039const listHeight = dimension.height - (72 + 11 + 14 /* header height + editor padding */);20402041this.splitView.el.style.height = `${listHeight}px`;20422043// We call layout first so the splitView has an idea of how much2044// space it has, otherwise setViewVisible results in the first panel2045// showing up at the minimum size whenever the Settings editor2046// opens for the first time.2047this.splitView.layout(this.bodyContainer.clientWidth, listHeight);20482049const tocBehavior = this.configurationService.getValue<'filter' | 'hide'>(SEARCH_TOC_BEHAVIOR_KEY);2050const hideTocForSearch = tocBehavior === 'hide' && this.searchResultModel;2051if (!hideTocForSearch) {2052const firstViewWasVisible = this.splitView.isViewVisible(0);2053const firstViewVisible = this.bodyContainer.clientWidth >= SettingsEditor2.NARROW_TOTAL_WIDTH;20542055this.splitView.setViewVisible(0, firstViewVisible);2056// If the first view is again visible, and we have enough space, immediately set the2057// editor to use the reset width rather than the cached min width2058if (!firstViewWasVisible && firstViewVisible && this.bodyContainer.clientWidth >= SettingsEditor2.EDITOR_MIN_WIDTH + SettingsEditor2.TOC_RESET_WIDTH) {2059this.splitView.resizeView(0, SettingsEditor2.TOC_RESET_WIDTH);2060}2061this.splitView.style({2062separatorBorder: firstViewVisible ? this.theme.getColor(settingsSashBorder)! : Color.transparent2063});2064}2065}20662067protected override saveState(): void {2068if (this.isVisible()) {2069const searchQuery = this.searchWidget.getValue().trim();2070const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget;2071if (this.input) {2072this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target });2073}2074} else if (this.input) {2075this.editorMemento.clearEditorState(this.input, this.group);2076}20772078super.saveState();2079}2080}20812082class SyncControls extends Disposable {2083private readonly lastSyncedLabel!: HTMLElement;2084private readonly turnOnSyncButton!: Button;20852086private readonly _onDidChangeLastSyncedLabel = this._register(new Emitter<string>());2087public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event;20882089constructor(2090window: CodeWindow,2091container: HTMLElement,2092@ICommandService private readonly commandService: ICommandService,2093@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,2094@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,2095@ITelemetryService telemetryService: ITelemetryService,2096) {2097super();20982099const headerRightControlsContainer = DOM.append(container, $('.settings-right-controls'));2100const turnOnSyncButtonContainer = DOM.append(headerRightControlsContainer, $('.turn-on-sync'));2101this.turnOnSyncButton = this._register(new Button(turnOnSyncButtonContainer, { title: true, ...defaultButtonStyles }));2102this.lastSyncedLabel = DOM.append(headerRightControlsContainer, $('.last-synced-label'));2103DOM.hide(this.lastSyncedLabel);21042105this.turnOnSyncButton.enabled = true;2106this.turnOnSyncButton.label = localize('turnOnSyncButton', "Backup and Sync Settings");2107DOM.hide(this.turnOnSyncButton.element);21082109this._register(this.turnOnSyncButton.onDidClick(async () => {2110await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn');2111}));21122113this.updateLastSyncedTime();2114this._register(this.userDataSyncService.onDidChangeLastSyncTime(() => {2115this.updateLastSyncedTime();2116}));21172118const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer());2119updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window);21202121this.update();2122this._register(this.userDataSyncService.onDidChangeStatus(() => {2123this.update();2124}));21252126this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => {2127this.update();2128}));2129}21302131private updateLastSyncedTime(): void {2132const last = this.userDataSyncService.lastSyncTime;2133let label: string;2134if (typeof last === 'number') {2135const d = fromNow(last, true, undefined, true);2136label = localize('lastSyncedLabel', "Last synced: {0}", d);2137} else {2138label = '';2139}21402141this.lastSyncedLabel.textContent = label;2142this._onDidChangeLastSyncedLabel.fire(label);2143}21442145private update(): void {2146if (this.userDataSyncService.status === SyncStatus.Uninitialized) {2147return;2148}21492150if (this.userDataSyncEnablementService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) {2151DOM.show(this.lastSyncedLabel);2152DOM.hide(this.turnOnSyncButton.element);2153} else {2154DOM.hide(this.lastSyncedLabel);2155DOM.show(this.turnOnSyncButton.element);2156}2157}2158}21592160interface ISettingsEditor2State {2161searchQuery: string;2162target: SettingsTarget;2163}216421652166