vscode/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts
2021-11-12 15:32:11 -08:00

1554 lines
59 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button } from 'vs/base/browser/ui/button/button';
import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
import { Action } from 'vs/base/common/actions';
import { Delayer, IntervalTimer, ThrottledDelayer, timeout } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import * as collections from 'vs/base/common/collections';
import { fromNow } from 'vs/base/common/date';
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
import { Emitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import { isArray, withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/settingsEditor2';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ConfigurationTarget, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { badgeBackground, badgeForeground, contrastBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IUserDataAutoSyncEnablementService, IUserDataSyncService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/common/editor';
import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput';
import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout';
import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveConfiguredUntrustedSettings, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree';
import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels';
import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/browser/settingsWidgets';
import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree';
import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences';
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { IUserDataSyncWorkbenchService } from 'vs/workbench/services/userDataSync/common/userDataSync';
import { preferencesClearInputIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export const enum SettingsFocusContext {
Search,
TableOfContents,
SettingTree,
SettingControl
}
export function createGroupIterator(group: SettingsTreeGroupElement): Iterable<ITreeElement<SettingsTreeGroupChild>> {
return Iterable.map(group.children, g => {
return {
element: g,
children: g instanceof SettingsTreeGroupElement ?
createGroupIterator(g) :
undefined
};
});
}
const $ = DOM.$;
interface IFocusEventFromScroll extends KeyboardEvent {
fromScroll: true;
}
const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings");
const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState';
export class SettingsEditor2 extends EditorPane {
static readonly ID: string = 'workbench.editor.settings2';
private static NUM_INSTANCES: number = 0;
private static SETTING_UPDATE_FAST_DEBOUNCE: number = 200;
private static SETTING_UPDATE_SLOW_DEBOUNCE: number = 1000;
private static CONFIG_SCHEMA_UPDATE_DELAYER = 500;
private static readonly SUGGESTIONS: string[] = [
`@${MODIFIED_SETTING_TAG}`,
'@tag:notebookLayout',
`@tag:${REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG}`,
`@tag:${WORKSPACE_TRUST_SETTING_TAG}`,
'@tag:sync',
'@tag:usesOnlineServices',
'@tag:telemetry',
`@${ID_SETTING_TAG}`,
`@${EXTENSION_SETTING_TAG}`,
`@${FEATURE_SETTING_TAG}scm`,
`@${FEATURE_SETTING_TAG}explorer`,
`@${FEATURE_SETTING_TAG}search`,
`@${FEATURE_SETTING_TAG}debug`,
`@${FEATURE_SETTING_TAG}extensions`,
`@${FEATURE_SETTING_TAG}terminal`,
`@${FEATURE_SETTING_TAG}task`,
`@${FEATURE_SETTING_TAG}problems`,
`@${FEATURE_SETTING_TAG}output`,
`@${FEATURE_SETTING_TAG}comments`,
`@${FEATURE_SETTING_TAG}remote`,
`@${FEATURE_SETTING_TAG}timeline`,
`@${FEATURE_SETTING_TAG}notebook`,
];
private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean {
if (isArray(type)) {
// nullable integer/number or complex
return false;
}
return type === SettingValueType.Enum ||
type === SettingValueType.Array ||
type === SettingValueType.BooleanObject ||
type === SettingValueType.Object ||
type === SettingValueType.Complex ||
type === SettingValueType.Boolean ||
type === SettingValueType.Exclude;
}
// (!) Lots of props that are set once on the first render
private defaultSettingsEditorModel!: Settings2EditorModel;
private modelDisposables: DisposableStore;
private rootElement!: HTMLElement;
private headerContainer!: HTMLElement;
private searchWidget!: SuggestEnabledInput;
private countElement!: HTMLElement;
private controlsElement!: HTMLElement;
private settingsTargetsWidget!: SettingsTargetsWidget;
private settingsTreeContainer!: HTMLElement;
private settingsTree!: SettingsTree;
private settingRenderers!: SettingTreeRenderers;
private tocTreeModel!: TOCTreeModel;
private settingsTreeModel!: SettingsTreeModel;
private noResultsMessage!: HTMLElement;
private clearFilterLinkContainer!: HTMLElement;
private tocTreeContainer!: HTMLElement;
private tocTree!: TOCTree;
private delayedFilterLogging: Delayer<void>;
private localSearchDelayer: Delayer<void>;
private remoteSearchThrottle: ThrottledDelayer<void>;
private searchInProgress: CancellationTokenSource | null = null;
private updatedConfigSchemaDelayer: Delayer<void>;
private settingFastUpdateDelayer: Delayer<void>;
private settingSlowUpdateDelayer: Delayer<void>;
private pendingSettingUpdate: { key: string, value: any } | null = null;
private readonly viewState: ISettingsEditorViewState;
private _searchResultModel: SearchResultModel | null = null;
private searchResultLabel: string | null = null;
private lastSyncedLabel: string | null = null;
private tocRowFocused: IContextKey<boolean>;
private settingRowFocused: IContextKey<boolean>;
private inSettingsEditorContextKey: IContextKey<boolean>;
private searchFocusContextKey: IContextKey<boolean>;
private scheduledRefreshes: Map<string, DOM.IFocusTracker>;
private _currentFocusContext: SettingsFocusContext = SettingsFocusContext.Search;
/** Don't spam warnings */
private hasWarnedMissingSettings = false;
private editorMemento: IEditorMemento<ISettingsEditor2State>;
private tocFocusedElement: SettingsTreeGroupElement | null = null;
private treeFocusedElement: SettingsTreeElement | null = null;
private settingsTreeScrollTop = 0;
private dimension!: DOM.Dimension;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService,
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
@IThemeService themeService: IThemeService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IPreferencesSearchService private readonly preferencesSearchService: IPreferencesSearchService,
@ILogService private readonly logService: ILogService,
@IContextKeyService contextKeyService: IContextKeyService,
@IStorageService storageService: IStorageService,
@IEditorGroupsService protected editorGroupService: IEditorGroupsService,
@IUserDataSyncWorkbenchService private readonly userDataSyncWorkbenchService: IUserDataSyncWorkbenchService,
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
@IExtensionService private readonly extensionService: IExtensionService
) {
super(SettingsEditor2.ID, telemetryService, themeService, storageService);
this.delayedFilterLogging = new Delayer<void>(1000);
this.localSearchDelayer = new Delayer(300);
this.remoteSearchThrottle = new ThrottledDelayer(200);
this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL };
this.settingFastUpdateDelayer = new Delayer<void>(SettingsEditor2.SETTING_UPDATE_FAST_DEBOUNCE);
this.settingSlowUpdateDelayer = new Delayer<void>(SettingsEditor2.SETTING_UPDATE_SLOW_DEBOUNCE);
this.updatedConfigSchemaDelayer = new Delayer<void>(SettingsEditor2.CONFIG_SCHEMA_UPDATE_DELAYER);
this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);
this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);
this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService);
this.settingRowFocused = CONTEXT_SETTINGS_ROW_FOCUS.bindTo(contextKeyService);
this.scheduledRefreshes = new Map<string, DOM.IFocusTracker>();
this.editorMemento = this.getEditorMemento<ISettingsEditor2State>(editorGroupService, textResourceConfigurationService, SETTINGS_EDITOR_STATE_KEY);
this._register(configurationService.onDidChangeConfiguration(e => {
if (e.source !== ConfigurationTarget.DEFAULT) {
this.onConfigUpdate(e.affectedKeys);
}
}));
this._register(workspaceTrustManagementService.onDidChangeTrust(() => {
if (this.searchResultModel) {
this.searchResultModel.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());
}
if (this.settingsTreeModel) {
this.settingsTreeModel.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted());
this.renderTree();
}
}));
this._register(configurationService.onDidChangeRestrictedSettings(e => {
if (e.default.length && this.currentSettingsModel) {
this.updateElementsByKey([...e.default]);
}
}));
this.modelDisposables = this._register(new DisposableStore());
}
override get minimumWidth(): number { return 375; }
override get maximumWidth(): number { return Number.POSITIVE_INFINITY; }
// these setters need to exist because this extends from EditorPane
override set minimumWidth(value: number) { /*noop*/ }
override set maximumWidth(value: number) { /*noop*/ }
private get currentSettingsModel() {
return this.searchResultModel || this.settingsTreeModel;
}
private get searchResultModel(): SearchResultModel | null {
return this._searchResultModel;
}
private set searchResultModel(value: SearchResultModel | null) {
this._searchResultModel = value;
this.rootElement.classList.toggle('search-mode', !!this._searchResultModel);
}
private get focusedSettingDOMElement(): HTMLElement | undefined {
const focused = this.settingsTree.getFocus()[0];
if (!(focused instanceof SettingsTreeSettingElement)) {
return;
}
return this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), focused.setting.key)[0];
}
get currentFocusContext() {
return this._currentFocusContext;
}
createEditor(parent: HTMLElement): void {
parent.setAttribute('tabindex', '-1');
this.rootElement = DOM.append(parent, $('.settings-editor', { tabindex: '-1' }));
this.createHeader(this.rootElement);
this.createBody(this.rootElement);
this.addCtrlAInterceptor(this.rootElement);
this.updateStyles();
}
override async setInput(input: SettingsEditor2Input, options: ISettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
this.inSettingsEditorContextKey.set(true);
await super.setInput(input, options, context, token);
await timeout(0); // Force setInput to be async
if (!this.input) {
return;
}
const model = await this.input.resolve();
if (token.isCancellationRequested || !(model instanceof Settings2EditorModel)) {
return;
}
this.modelDisposables.clear();
this.modelDisposables.add(model.onDidChangeGroups(() => {
this.updatedConfigSchemaDelayer.trigger(() => {
this.onConfigUpdate(undefined, false, true);
});
}));
this.defaultSettingsEditorModel = model;
options = options || validateSettingsEditorOptions({});
if (!this.viewState.settingsTarget) {
if (!options.target) {
options.target = ConfigurationTarget.USER_LOCAL;
}
}
this._setOptions(options);
// Don't block setInput on render (which can trigger an async search)
this.onConfigUpdate(undefined, true).then(() => {
this._register(input.onWillDispose(() => {
this.searchWidget.setValue('');
}));
// Init TOC selection
this.updateTreeScrollSync();
});
}
private restoreCachedState(): ISettingsEditor2State | null {
const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input);
if (cachedState && typeof cachedState.target === 'object') {
cachedState.target = URI.revive(cachedState.target);
}
if (cachedState) {
const settingsTarget = cachedState.target;
this.settingsTargetsWidget.settingsTarget = settingsTarget;
this.viewState.settingsTarget = settingsTarget;
this.searchWidget.setValue(cachedState.searchQuery);
}
if (this.input) {
this.editorMemento.clearEditorState(this.input, this.group);
}
return withUndefinedAsNull(cachedState);
}
override setOptions(options: ISettingsEditorOptions | undefined): void {
super.setOptions(options);
if (options) {
this._setOptions(options);
}
}
private _setOptions(options: ISettingsEditorOptions): void {
if (options.focusSearch && !platform.isIOS) {
// isIOS - #122044
this.focusSearch();
}
if (options.query) {
this.searchWidget.setValue(options.query);
}
const target: SettingsTarget = options.folderUri || <SettingsTarget>options.target;
if (target) {
this.settingsTargetsWidget.settingsTarget = target;
this.viewState.settingsTarget = target;
}
}
override clearInput(): void {
this.inSettingsEditorContextKey.set(false);
super.clearInput();
}
layout(dimension: DOM.Dimension): void {
this.dimension = dimension;
if (!this.isVisible()) {
return;
}
this.layoutTrees(dimension);
const innerWidth = Math.min(1000, dimension.width) - 24 * 2; // 24px padding on left and right;
// minus padding inside inputbox, countElement width, controls width, extra padding before countElement
const monacoWidth = innerWidth - 10 - this.countElement.clientWidth - this.controlsElement.clientWidth - 12;
this.searchWidget.layout(new DOM.Dimension(monacoWidth, 20));
this.rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600);
this.rootElement.classList.toggle('narrow-width', dimension.width < 600);
}
override focus(): void {
if (this._currentFocusContext === SettingsFocusContext.Search) {
if (!platform.isIOS) {
// #122044
this.focusSearch();
}
} else if (this._currentFocusContext === SettingsFocusContext.SettingControl) {
const element = this.focusedSettingDOMElement;
if (element) {
const control = element.querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
if (control) {
(<HTMLElement>control).focus();
return;
}
}
} else if (this._currentFocusContext === SettingsFocusContext.SettingTree) {
this.settingsTree.domFocus();
} else if (this._currentFocusContext === SettingsFocusContext.TableOfContents) {
this.tocTree.domFocus();
}
}
protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void {
super.setEditorVisible(visible, group);
if (!visible) {
// Wait for editor to be removed from DOM #106303
setTimeout(() => {
this.searchWidget.onHide();
}, 0);
}
}
focusSettings(focusSettingInput = false): void {
const focused = this.settingsTree.getFocus();
if (!focused.length) {
this.settingsTree.focusFirst();
}
this.settingsTree.domFocus();
if (focusSettingInput) {
const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`);
if (controlInFocusedRow) {
(<HTMLElement>controlInFocusedRow).focus();
}
}
}
focusTOC(): void {
this.tocTree.domFocus();
}
showContextMenu(): void {
const focused = this.settingsTree.getFocus()[0];
const rowElement = this.focusedSettingDOMElement;
if (rowElement && focused instanceof SettingsTreeSettingElement) {
this.settingRenderers.showContextMenu(focused, rowElement);
}
}
focusSearch(filter?: string, selectAll = true): void {
if (filter && this.searchWidget) {
this.searchWidget.setValue(filter);
}
this.searchWidget.focus(selectAll);
}
clearSearchResults(): void {
this.searchWidget.setValue('');
this.focusSearch();
}
clearSearchFilters(): void {
let query = this.searchWidget.getValue();
SettingsEditor2.SUGGESTIONS.forEach(suggestion => {
query = query.replace(suggestion, '');
});
this.searchWidget.setValue(query.trim());
}
private updateInputAriaLabel() {
let label = searchBoxLabel;
if (this.searchResultLabel) {
label += `. ${this.searchResultLabel}`;
}
if (this.lastSyncedLabel) {
label += `. ${this.lastSyncedLabel}`;
}
this.searchWidget.updateAriaLabel(label);
}
private createHeader(parent: HTMLElement): void {
this.headerContainer = DOM.append(parent, $('.settings-header'));
const searchContainer = DOM.append(this.headerContainer, $('.search-container'));
const clearInputAction = new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Settings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults());
this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, {
triggerCharacters: ['@'],
provideResults: (query: string) => {
return SettingsEditor2.SUGGESTIONS.filter(tag => query.indexOf(tag) === -1).map(tag => tag.endsWith(':') ? tag : tag + ' ');
}
}, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, {
placeholderText: searchBoxLabel,
focusContextKey: this.searchFocusContextKey,
// TODO: Aria-live
}));
this._register(this.searchWidget.onFocus(() => {
this._currentFocusContext = SettingsFocusContext.Search;
}));
this._register(attachSuggestEnabledInputBoxStyler(this.searchWidget, this.themeService, {
inputBorder: settingsTextInputBorder
}));
this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget.monaco-count-badge.long'));
this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';
const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : '';
this.countElement.style.backgroundColor = background;
this.countElement.style.color = foreground;
this.countElement.style.borderWidth = border ? '1px' : '';
this.countElement.style.borderStyle = border ? 'solid' : '';
this.countElement.style.borderColor = border;
}));
this._register(this.searchWidget.onInputDidChange(() => {
const searchVal = this.searchWidget.getValue();
clearInputAction.enabled = !!searchVal;
this.onSearchInputChanged();
}));
const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true }));
this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL;
this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target));
this._register(DOM.addDisposableListener(targetWidgetContainer, DOM.EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
if (event.keyCode === KeyCode.DownArrow) {
this.focusSettings();
}
}));
if (this.userDataSyncWorkbenchService.enabled && this.userDataAutoSyncEnablementService.canToggleEnablement()) {
const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer));
this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => {
this.lastSyncedLabel = lastSyncedLabel;
this.updateInputAriaLabel();
}));
}
this.controlsElement = DOM.append(searchContainer, DOM.$('.settings-clear-widget'));
const actionBar = this._register(new ActionBar(this.controlsElement, {
animated: false,
actionViewItemProvider: (_action) => { return undefined; }
}));
actionBar.push([clearInputAction], { label: false, icon: true });
}
private onDidSettingsTargetChange(target: SettingsTarget): void {
this.viewState.settingsTarget = target;
// TODO Instead of rebuilding the whole model, refresh and uncache the inspected setting value
this.onConfigUpdate(undefined, true);
}
private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void {
const elements = this.currentSettingsModel.getElementsByName(evt.targetKey);
if (elements && elements[0]) {
let sourceTop = 0.5;
try {
const _sourceTop = this.settingsTree.getRelativeTop(evt.source);
if (_sourceTop !== null) {
sourceTop = _sourceTop;
}
} catch {
// e.g. clicked a searched element, now the search has been cleared
}
this.settingsTree.reveal(elements[0], sourceTop);
// We need to shift focus from the setting that contains the link to the setting that's
// linked. Clicking on the link sets focus on the setting that contains the link,
// which is why we need the setTimeout
setTimeout(() => this.settingsTree.setFocus([elements[0]]), 50);
const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey);
if (domElements && domElements[0]) {
const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
if (control) {
(<HTMLElement>control).focus();
}
}
} else if (!recursed) {
const p = this.triggerSearch('');
p.then(() => {
this.searchWidget.setValue('');
this.onDidClickSetting(evt, true);
});
}
}
switchToSettingsFile(): Promise<IEditorPane | undefined> {
const query = parseQuery(this.searchWidget.getValue()).query;
return this.openSettingsFile({ query });
}
private async openSettingsFile(options?: ISettingsEditorOptions): Promise<IEditorPane | undefined> {
const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget;
const openOptions: IOpenSettingsOptions = { jsonEditor: true, ...options };
if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) {
return this.preferencesService.openUserSettings(openOptions);
} else if (currentSettingsTarget === ConfigurationTarget.USER_REMOTE) {
return this.preferencesService.openRemoteSettings(openOptions);
} else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) {
return this.preferencesService.openWorkspaceSettings(openOptions);
} else if (URI.isUri(currentSettingsTarget)) {
return this.preferencesService.openFolderSettings({ folderUri: currentSettingsTarget, ...openOptions });
}
return undefined;
}
private createBody(parent: HTMLElement): void {
const bodyContainer = DOM.append(parent, $('.settings-body'));
this.noResultsMessage = DOM.append(bodyContainer, $('.no-results-message'));
this.noResultsMessage.innerText = localize('noResults', "No Settings Found");
this.clearFilterLinkContainer = $('span.clear-search-filters');
this.clearFilterLinkContainer.textContent = ' - ';
const clearFilterLink = DOM.append(this.clearFilterLinkContainer, $('a.pointer.prominent', { tabindex: 0 }, localize('clearSearchFilters', 'Clear Filters')));
this._register(DOM.addDisposableListener(clearFilterLink, DOM.EventType.CLICK, (e: MouseEvent) => {
DOM.EventHelper.stop(e, false);
this.clearSearchFilters();
}));
DOM.append(this.noResultsMessage, this.clearFilterLinkContainer);
this._register(attachStylerCallback(this.themeService, { editorForeground }, colors => {
this.noResultsMessage.style.color = colors.editorForeground ? colors.editorForeground.toString() : '';
}));
this.createTOC(bodyContainer);
this.createSettingsTree(bodyContainer);
}
private addCtrlAInterceptor(container: HTMLElement): void {
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
if (
e.keyCode === KeyCode.KeyA &&
(platform.isMacintosh ? e.metaKey : e.ctrlKey) &&
e.target.tagName !== 'TEXTAREA' &&
e.target.tagName !== 'INPUT'
) {
// Avoid browser ctrl+a
e.browserEvent.stopPropagation();
e.browserEvent.preventDefault();
}
}));
}
private createTOC(parent: HTMLElement): void {
this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState);
this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container'));
this.tocTree = this._register(this.instantiationService.createInstance(TOCTree,
DOM.append(this.tocTreeContainer, $('.settings-toc-wrapper', {
'role': 'navigation',
'aria-label': localize('settings', "Settings"),
})),
this.viewState));
this._register(this.tocTree.onDidFocus(() => {
this._currentFocusContext = SettingsFocusContext.TableOfContents;
}));
this._register(this.tocTree.onDidChangeFocus(e => {
const element: SettingsTreeGroupElement | null = e.elements[0];
if (this.tocFocusedElement === element) {
return;
}
this.tocFocusedElement = element;
this.tocTree.setSelection(element ? [element] : []);
if (this.searchResultModel) {
if (this.viewState.filterToCategory !== element) {
this.viewState.filterToCategory = withNullAsUndefined(element);
this.renderTree();
this.settingsTree.scrollTop = 0;
}
} else if (element && (!e.browserEvent || !(<IFocusEventFromScroll>e.browserEvent).fromScroll)) {
this.settingsTree.reveal(element, 0);
this.settingsTree.setFocus([element]);
}
}));
this._register(this.tocTree.onDidFocus(() => {
this.tocRowFocused.set(true);
}));
this._register(this.tocTree.onDidBlur(() => {
this.tocRowFocused.set(false);
}));
}
private createSettingsTree(parent: HTMLElement): void {
this.settingsTreeContainer = DOM.append(parent, $('.settings-tree-container'));
this.settingRenderers = this.instantiationService.createInstance(SettingTreeRenderers);
this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type)));
this._register(this.settingRenderers.onDidOpenSettings(settingKey => {
this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } });
}));
this._register(this.settingRenderers.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName)));
this._register(this.settingRenderers.onDidFocusSetting(element => {
this.settingsTree.setFocus([element]);
this._currentFocusContext = SettingsFocusContext.SettingControl;
this.settingRowFocused.set(false);
}));
this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => {
if (element.scope.toLowerCase() === 'workspace') {
this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE);
} else if (element.scope.toLowerCase() === 'user') {
this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL);
} else if (element.scope.toLowerCase() === 'remote') {
this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE);
}
this.searchWidget.setValue(element.targetKey);
}));
this._register(this.settingRenderers.onDidChangeSettingHeight((params: HeightChangeParams) => {
const { element, height } = params;
try {
this.settingsTree.updateElementHeight(element, height);
} catch (e) {
// the element was not found
}
}));
this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree,
this.settingsTreeContainer,
this.viewState,
this.settingRenderers.allRenderers));
this._register(this.settingsTree.onDidScroll(() => {
if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) {
return;
}
this.settingsTreeScrollTop = this.settingsTree.scrollTop;
// setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when
// setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync
setTimeout(() => {
this.updateTreeScrollSync();
}, 0);
}));
this._register(this.settingsTree.onDidFocus(() => {
if (document.activeElement?.classList.contains('monaco-list')) {
this._currentFocusContext = SettingsFocusContext.SettingTree;
this.settingRowFocused.set(true);
}
}));
this._register(this.settingsTree.onDidBlur(() => {
this.settingRowFocused.set(false);
}));
// There is no different select state in the settings tree
this._register(this.settingsTree.onDidChangeFocus(e => {
const element = e.elements[0];
if (this.treeFocusedElement === element) {
return;
}
if (this.treeFocusedElement) {
this.treeFocusedElement.tabbable = false;
}
this.treeFocusedElement = element;
if (this.treeFocusedElement) {
this.treeFocusedElement.tabbable = true;
}
this.settingsTree.setSelection(element ? [element] : []);
}));
}
private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[]): void {
if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) {
this.updateChangedSetting(key, value);
}
this.pendingSettingUpdate = { key, value };
if (SettingsEditor2.shouldSettingUpdateFast(type)) {
this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
} else {
this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
}
}
private updateTreeScrollSync(): void {
this.settingRenderers.cancelSuggesters();
if (this.searchResultModel) {
return;
}
if (!this.tocTreeModel) {
return;
}
const elementToSync = this.settingsTree.firstVisibleElement;
const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent :
elementToSync instanceof SettingsTreeGroupElement ? elementToSync :
null;
// It's possible for this to be called when the TOC and settings tree are out of sync - e.g. when the settings tree has deferred a refresh because
// it is focused. So, bail if element doesn't exist in the TOC.
let nodeExists = true;
try { this.tocTree.getNode(element); } catch (e) { nodeExists = false; }
if (!nodeExists) {
return;
}
if (element && this.tocTree.getSelection()[0] !== element) {
const ancestors = this.getAncestors(element);
ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));
this.tocTree.reveal(element);
const elementTop = this.tocTree.getRelativeTop(element);
if (typeof elementTop !== 'number') {
return;
}
this.tocTree.collapseAll();
ancestors.forEach(e => this.tocTree.expand(<SettingsTreeGroupElement>e));
if (elementTop < 0 || elementTop > 1) {
this.tocTree.reveal(element);
} else {
this.tocTree.reveal(element, elementTop);
}
this.tocTree.expand(element);
this.tocTree.setSelection([element]);
const fakeKeyboardEvent = new KeyboardEvent('keydown');
(<IFocusEventFromScroll>fakeKeyboardEvent).fromScroll = true;
this.tocTree.setFocus([element], fakeKeyboardEvent);
}
}
private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] {
const ancestors: any[] = [];
while (element.parent) {
if (element.parent.id !== 'root') {
ancestors.push(element.parent);
}
element = element.parent;
}
return ancestors.reverse();
}
private updateChangedSetting(key: string, value: any): Promise<void> {
// ConfigurationService displays the error if this fails.
// Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change
const settingsTarget = this.settingsTargetsWidget.settingsTarget;
const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined;
const configurationTarget = <ConfigurationTarget>(resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget);
const overrides: IConfigurationOverrides = { resource };
const isManualReset = value === undefined;
// If the user is changing the value back to the default, do a 'reset' instead
const inspected = this.configurationService.inspect(key, overrides);
if (inspected.defaultValue === value) {
value = undefined;
}
return this.configurationService.updateValue(key, value, overrides, configurationTarget)
.then(() => {
this.renderTree(key, isManualReset);
const reportModifiedProps = {
key,
query: this.searchWidget.getValue(),
searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(),
rawResults: this.searchResultModel && this.searchResultModel.getRawResults(),
showConfiguredOnly: !!this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG),
isReset: typeof value === 'undefined',
settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget
};
return this.reportModifiedSetting(reportModifiedProps);
});
}
private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[] | null, rawResults: ISearchResult[] | null, showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void {
this.pendingSettingUpdate = null;
let groupId: string | undefined = undefined;
let nlpIndex: number | undefined = undefined;
let displayIndex: number | undefined = undefined;
if (props.searchResults) {
const remoteResult = props.searchResults[SearchResultIdx.Remote];
const localResult = props.searchResults[SearchResultIdx.Local];
const localIndex = localResult!.filterMatches.findIndex(m => m.setting.key === props.key);
groupId = localIndex >= 0 ?
'local' :
'remote';
displayIndex = localIndex >= 0 ?
localIndex :
remoteResult && (remoteResult.filterMatches.findIndex(m => m.setting.key === props.key) + localResult.filterMatches.length);
if (this.searchResultModel) {
const rawResults = this.searchResultModel.getRawResults();
if (rawResults[SearchResultIdx.Remote]) {
const _nlpIndex = rawResults[SearchResultIdx.Remote].filterMatches.findIndex(m => m.setting.key === props.key);
nlpIndex = _nlpIndex >= 0 ? _nlpIndex : undefined;
}
}
}
const reportedTarget = props.settingsTarget === ConfigurationTarget.USER_LOCAL ? 'user' :
props.settingsTarget === ConfigurationTarget.USER_REMOTE ? 'user_remote' :
props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' :
'folder';
const data = {
key: props.key,
groupId,
nlpIndex,
displayIndex,
showConfiguredOnly: props.showConfiguredOnly,
isReset: props.isReset,
target: reportedTarget
};
/* __GDPR__
"settingsEditor.settingModified" : {
"key" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"groupId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"nlpIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"displayIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"showConfiguredOnly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"isReset" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('settingsEditor.settingModified', data);
}
private onSearchModeToggled(): void {
this.rootElement.classList.remove('no-toc-search');
if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') {
this.rootElement.classList.toggle('no-toc-search', !!this.searchResultModel);
}
}
private scheduleRefresh(element: HTMLElement, key = ''): void {
if (key && this.scheduledRefreshes.has(key)) {
return;
}
if (!key) {
this.scheduledRefreshes.forEach(r => r.dispose());
this.scheduledRefreshes.clear();
}
const scheduledRefreshTracker = DOM.trackFocus(element);
this.scheduledRefreshes.set(key, scheduledRefreshTracker);
scheduledRefreshTracker.onDidBlur(() => {
scheduledRefreshTracker.dispose();
this.scheduledRefreshes.delete(key);
this.onConfigUpdate([key]);
});
}
private async onConfigUpdate(keys?: string[], forceRefresh = false, schemaChange = false): Promise<void> {
if (keys && this.settingsTreeModel) {
return this.updateElementsByKey(keys);
}
const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
const dividedGroups = collections.groupBy(groups, g => g.extensionInfo ? 'extension' : 'core');
const settingsResult = resolveSettingsTree(tocData, dividedGroups.core, this.logService);
const resolvedSettingsRoot = settingsResult.tree;
// Warn for settings not included in layout
if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) {
const settingKeyList: string[] = [];
settingsResult.leftoverSettings.forEach(s => {
settingKeyList.push(s.key);
});
this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`);
this.hasWarnedMissingSettings = true;
}
const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core, this.logService);
resolvedSettingsRoot.children!.unshift(commonlyUsed.tree);
resolvedSettingsRoot.children!.push(await resolveExtensionsSettings(this.extensionService, dividedGroups.extension || []));
if (!this.workspaceTrustManagementService.isWorkspaceTrusted() && (this.viewState.settingsTarget instanceof URI || this.viewState.settingsTarget === ConfigurationTarget.WORKSPACE)) {
const configuredUntrustedWorkspaceSettings = resolveConfiguredUntrustedSettings(groups, this.viewState.settingsTarget, this.configurationService);
if (configuredUntrustedWorkspaceSettings.length) {
resolvedSettingsRoot.children!.unshift({
id: 'workspaceTrust',
label: localize('settings require trust', "Workspace Trust"),
settings: configuredUntrustedWorkspaceSettings
});
}
}
if (this.searchResultModel) {
this.searchResultModel.updateChildren();
}
if (this.settingsTreeModel) {
this.settingsTreeModel.update(resolvedSettingsRoot);
if (schemaChange && !!this.searchResultModel) {
// If an extension's settings were just loaded and a search is active, retrigger the search so it shows up
return await this.onSearchInputChanged();
}
this.refreshTOCTree();
this.renderTree(undefined, forceRefresh);
} else {
this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());
this.settingsTreeModel.update(resolvedSettingsRoot);
this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement;
const cachedState = this.restoreCachedState();
if (cachedState && cachedState.searchQuery || !!this.searchWidget.getValue()) {
await this.onSearchInputChanged();
} else {
this.refreshTOCTree();
this.refreshTree();
this.tocTree.collapseAll();
}
}
}
private updateElementsByKey(keys: string[]): void {
if (keys.length) {
if (this.searchResultModel) {
keys.forEach(key => this.searchResultModel!.updateElementsByName(key));
}
if (this.settingsTreeModel) {
keys.forEach(key => this.settingsTreeModel.updateElementsByName(key));
}
keys.forEach(key => this.renderTree(key));
} else {
return this.renderTree();
}
}
private getActiveControlInSettingsTree(): HTMLElement | null {
return (document.activeElement && DOM.isAncestor(document.activeElement, this.settingsTree.getHTMLElement())) ?
<HTMLElement>document.activeElement :
null;
}
private renderTree(key?: string, force = false): void {
if (!force && key && this.scheduledRefreshes.has(key)) {
this.updateModifiedLabelForKey(key);
return;
}
// If the context view is focused, delay rendering settings
if (this.contextViewFocused()) {
const element = document.querySelector('.context-view');
if (element) {
this.scheduleRefresh(element as HTMLElement, key);
}
return;
}
// If a setting control is currently focused, schedule a refresh for later
const activeElement = this.getActiveControlInSettingsTree();
const focusedSetting = activeElement && this.settingRenderers.getSettingDOMElementForDOMElement(activeElement);
if (focusedSetting && !force) {
// If a single setting is being refreshed, it's ok to refresh now if that is not the focused setting
if (key) {
const focusedKey = focusedSetting.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR);
if (focusedKey === key &&
// update `list`s live, as they have a separate "submit edit" step built in before this
(focusedSetting.parentElement && !focusedSetting.parentElement.classList.contains('setting-item-list'))
) {
this.updateModifiedLabelForKey(key);
this.scheduleRefresh(focusedSetting, key);
return;
}
} else {
this.scheduleRefresh(focusedSetting);
return;
}
}
this.renderResultCountMessages();
if (key) {
const elements = this.currentSettingsModel.getElementsByName(key);
if (elements && elements.length) {
// TODO https://github.com/microsoft/vscode/issues/57360
this.refreshTree();
} else {
// Refresh requested for a key that we don't know about
return;
}
} else {
this.refreshTree();
}
return;
}
private contextViewFocused(): boolean {
return !!DOM.findParentWithClass(<HTMLElement>document.activeElement, 'context-view');
}
private refreshTree(): void {
if (this.isVisible()) {
this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root));
}
}
private refreshTOCTree(): void {
if (this.isVisible()) {
this.tocTreeModel.update();
this.tocTree.setChildren(null, createTOCIterator(this.tocTreeModel, this.tocTree));
}
}
private updateModifiedLabelForKey(key: string): void {
const dataElements = this.currentSettingsModel.getElementsByName(key);
const isModified = dataElements && dataElements[0] && dataElements[0].isConfigured; // all elements are either configured or not
const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key);
if (elements && elements[0]) {
elements[0].classList.toggle('is-configured', !!isModified);
}
}
private async onSearchInputChanged(): Promise<void> {
if (!this.currentSettingsModel) {
// Initializing search widget value
return;
}
const query = this.searchWidget.getValue().trim();
this.delayedFilterLogging.cancel();
// allow-any-unicode-next-line
await this.triggerSearch(query.replace(//g, ' '));
if (query && this.searchResultModel) {
this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel!.getUniqueResults()));
}
}
private parseSettingFromJSON(query: string): string | null {
const match = query.match(/"([a-zA-Z.]+)": /);
return match && match[1];
}
private triggerSearch(query: string): Promise<void> {
this.viewState.tagFilters = new Set<string>();
this.viewState.extensionFilters = new Set<string>();
this.viewState.featureFilters = new Set<string>();
this.viewState.idFilters = new Set<string>();
if (query) {
const parsedQuery = parseQuery(query);
query = parsedQuery.query;
parsedQuery.tags.forEach(tag => this.viewState.tagFilters!.add(tag));
parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId));
parsedQuery.featureFilters!.forEach(feature => this.viewState.featureFilters!.add(feature));
parsedQuery.idFilters!.forEach(id => this.viewState.idFilters!.add(id));
}
if (query && query !== '@') {
query = this.parseSettingFromJSON(query) || query;
return this.triggerFilterPreferences(query);
} else {
if (this.viewState.tagFilters.size || this.viewState.extensionFilters.size || this.viewState.featureFilters.size || this.viewState.idFilters.size) {
this.searchResultModel = this.createFilterModel();
} else {
this.searchResultModel = null;
}
this.localSearchDelayer.cancel();
this.remoteSearchThrottle.cancel();
if (this.searchInProgress) {
this.searchInProgress.cancel();
this.searchInProgress.dispose();
this.searchInProgress = null;
}
this.tocTree.setFocus([]);
this.viewState.filterToCategory = undefined;
this.tocTreeModel.currentSearchModel = this.searchResultModel;
this.onSearchModeToggled();
if (this.searchResultModel) {
// Added a filter model
this.tocTree.setSelection([]);
this.tocTree.expandAll();
this.refreshTOCTree();
this.renderResultCountMessages();
this.refreshTree();
} else {
// Leaving search mode
this.tocTree.collapseAll();
this.refreshTOCTree();
this.renderResultCountMessages();
this.refreshTree();
}
}
return Promise.resolve();
}
/**
* Return a fake SearchResultModel which can hold a flat list of all settings, to be filtered (@modified etc)
*/
private createFilterModel(): SearchResultModel {
const filterModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());
const fullResult: ISearchResult = {
filterMatches: []
};
for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) {
for (const sect of g.sections) {
for (const setting of sect.settings) {
fullResult.filterMatches.push({ setting, matches: [], matchType: SettingMatchType.None, score: 0 });
}
}
}
filterModel.setResult(0, fullResult);
return filterModel;
}
private reportFilteringUsed(query: string, results: ISearchResult[]): void {
const nlpResult = results[SearchResultIdx.Remote];
const nlpMetadata = nlpResult && nlpResult.metadata;
const durations = {
nlpResult: nlpMetadata && nlpMetadata.duration
};
// Count unique results
const counts: { nlpResult?: number, filterResult?: number } = {};
const filterResult = results[SearchResultIdx.Local];
if (filterResult) {
counts['filterResult'] = filterResult.filterMatches.length;
}
if (nlpResult) {
counts['nlpResult'] = nlpResult.filterMatches.length;
}
const requestCount = nlpMetadata && nlpMetadata.requestCount;
const data = {
durations,
counts,
requestCount
};
/* __GDPR__
"settingsEditor.filter" : {
"durations.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"counts.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"counts.filterResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"requestCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this.telemetryService.publicLog('settingsEditor.filter', data);
}
private triggerFilterPreferences(query: string): Promise<void> {
if (this.searchInProgress) {
this.searchInProgress.cancel();
this.searchInProgress = null;
}
// Trigger the local search. If it didn't find an exact match, trigger the remote search.
const searchInProgress = this.searchInProgress = new CancellationTokenSource();
return this.localSearchDelayer.trigger(() => {
if (searchInProgress && !searchInProgress.token.isCancellationRequested) {
return this.localFilterPreferences(query).then(result => {
if (result && !result.exactMatch) {
this.remoteSearchThrottle.trigger(() => {
return searchInProgress && !searchInProgress.token.isCancellationRequested ?
this.remoteSearchPreferences(query, this.searchInProgress!.token) :
Promise.resolve();
});
}
});
} else {
return Promise.resolve();
}
});
}
private localFilterPreferences(query: string, token?: CancellationToken): Promise<ISearchResult | null> {
const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider, token);
}
private remoteSearchPreferences(query: string, token?: CancellationToken): Promise<void> {
const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
const newExtSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query, true);
return Promise.all([
this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider, token),
this.filterOrSearchPreferences(query, SearchResultIdx.NewExtensions, newExtSearchProvider, token)
]).then(() => { });
}
private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise<ISearchResult | null> {
return this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token).then(result => {
if (token && token.isCancellationRequested) {
// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
return null;
}
if (!this.searchResultModel) {
this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted());
this.searchResultModel.setResult(type, result);
this.tocTreeModel.currentSearchModel = this.searchResultModel;
this.onSearchModeToggled();
} else {
this.searchResultModel.setResult(type, result);
this.tocTreeModel.update();
}
if (type === SearchResultIdx.Local) {
this.tocTree.setFocus([]);
this.viewState.filterToCategory = undefined;
this.tocTree.expandAll();
}
this.settingsTree.scrollTop = 0;
this.refreshTOCTree();
this.renderTree(undefined, true);
return result;
});
}
private renderResultCountMessages() {
if (!this.currentSettingsModel) {
return;
}
this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 0
? 'initial'
: 'none';
if (!this.searchResultModel) {
if (this.countElement.style.display !== 'none') {
this.searchResultLabel = null;
this.countElement.style.display = 'none';
this.layout(this.dimension);
}
this.rootElement.classList.remove('no-results');
return;
}
if (this.tocTreeModel && this.tocTreeModel.settingsTreeRoot) {
const count = this.tocTreeModel.settingsTreeRoot.count;
let resultString: string;
switch (count) {
case 0: resultString = localize('noResults', "No Settings Found"); break;
case 1: resultString = localize('oneResult', "1 Setting Found"); break;
default: resultString = localize('moreThanOneResult', "{0} Settings Found", count);
}
this.searchResultLabel = resultString;
this.updateInputAriaLabel();
this.countElement.innerText = resultString;
aria.status(resultString);
if (this.countElement.style.display !== 'block') {
this.countElement.style.display = 'block';
this.layout(this.dimension);
}
this.rootElement.classList.toggle('no-results', count === 0);
}
}
private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider?: ISearchProvider, token?: CancellationToken): Promise<ISearchResult | null> {
const searchP = provider ? provider.searchModel(model, token) : Promise.resolve(null);
return searchP
.then<ISearchResult, ISearchResult | null>(undefined, err => {
if (isPromiseCanceledError(err)) {
return Promise.reject(err);
} else {
/* __GDPR__
"settingsEditor.searchError" : {
"message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" }
}
*/
const message = getErrorMessage(err).trim();
if (message && message !== 'Error') {
// "Error" = any generic network error
this.telemetryService.publicLogError('settingsEditor.searchError', { message });
this.logService.info('Setting search error: ' + message);
}
return null;
}
});
}
private layoutTrees(dimension: DOM.Dimension): void {
const listHeight = dimension.height - (72 + 11 /* header height + editor padding */);
const settingsTreeHeight = listHeight - 14;
this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`;
this.settingsTree.layout(settingsTreeHeight, dimension.width);
const tocTreeHeight = settingsTreeHeight - 1;
this.tocTreeContainer.style.height = `${tocTreeHeight}px`;
this.tocTree.layout(tocTreeHeight);
}
protected override saveState(): void {
if (this.isVisible()) {
const searchQuery = this.searchWidget.getValue().trim();
const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget;
if (this.group && this.input) {
this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target });
}
}
super.saveState();
}
}
class SyncControls extends Disposable {
private readonly lastSyncedLabel!: HTMLElement;
private readonly turnOnSyncButton!: Button;
private readonly _onDidChangeLastSyncedLabel = this._register(new Emitter<string>());
public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event;
constructor(
container: HTMLElement,
@ICommandService private readonly commandService: ICommandService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IThemeService themeService: IThemeService,
) {
super();
const headerRightControlsContainer = DOM.append(container, $('.settings-right-controls'));
const turnOnSyncButtonContainer = DOM.append(headerRightControlsContainer, $('.turn-on-sync'));
this.turnOnSyncButton = this._register(new Button(turnOnSyncButtonContainer, { title: true }));
this._register(attachButtonStyler(this.turnOnSyncButton, themeService));
this.lastSyncedLabel = DOM.append(headerRightControlsContainer, $('.last-synced-label'));
DOM.hide(this.lastSyncedLabel);
this.turnOnSyncButton.enabled = true;
this.turnOnSyncButton.label = localize('turnOnSyncButton', "Turn on Settings Sync");
DOM.hide(this.turnOnSyncButton.element);
this._register(this.turnOnSyncButton.onDidClick(async () => {
await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn');
}));
this.updateLastSyncedTime();
this._register(this.userDataSyncService.onDidChangeLastSyncTime(() => {
this.updateLastSyncedTime();
}));
const updateLastSyncedTimer = this._register(new IntervalTimer());
updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000);
this.update();
this._register(this.userDataSyncService.onDidChangeStatus(() => {
this.update();
}));
this._register(this.userDataAutoSyncEnablementService.onDidChangeEnablement(() => {
this.update();
}));
}
private updateLastSyncedTime(): void {
const last = this.userDataSyncService.lastSyncTime;
let label: string;
if (typeof last === 'number') {
const d = fromNow(last, true);
label = localize('lastSyncedLabel', "Last synced: {0}", d);
} else {
label = '';
}
this.lastSyncedLabel.textContent = label;
this._onDidChangeLastSyncedLabel.fire(label);
}
private update(): void {
if (this.userDataSyncService.status === SyncStatus.Uninitialized) {
return;
}
if (this.userDataAutoSyncEnablementService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) {
DOM.show(this.lastSyncedLabel);
DOM.hide(this.turnOnSyncButton.element);
} else {
DOM.hide(this.lastSyncedLabel);
DOM.show(this.turnOnSyncButton.element);
}
}
}
interface ISettingsEditor2State {
searchQuery: string;
target: SettingsTarget;
}