vscode/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts
2021-11-20 21:01:29 +01:00

1553 lines
58 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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, 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();
await this.triggerSearch(query.replace(/\u203A/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: [], 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;
}