settingsEditor2.ts 48.9 KB
Newer Older
R
Rob Lourens 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  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';
R
Rob Lourens 已提交
7
import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
8
import * as arrays from 'vs/base/common/arrays';
9
import { Delayer, ThrottledDelayer, timeout } from 'vs/base/common/async';
10
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
R
Rob Lourens 已提交
11
import * as collections from 'vs/base/common/collections';
12
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
R
Rob Lourens 已提交
13
import { Iterator } from 'vs/base/common/iterator';
P
Peng Lyu 已提交
14
import * as strings from 'vs/base/common/strings';
15
import { isArray, withNullAsUndefined } from 'vs/base/common/types';
16
import { URI } from 'vs/base/common/uri';
R
Rob Lourens 已提交
17 18
import 'vs/css!./media/settingsEditor2';
import { localize } from 'vs/nls';
19
import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration';
R
Rob Lourens 已提交
20
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
R
Rob Lourens 已提交
21
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
R
Rob Lourens 已提交
22
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
23
import { ILogService } from 'vs/platform/log/common/log';
J
Jackson Kearl 已提交
24
import { INotificationService } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
25
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
R
Rob Lourens 已提交
26
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
J
Jackson Kearl 已提交
27
import { badgeBackground, badgeForeground, contrastBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry';
28
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
29
import { IThemeService } from 'vs/platform/theme/common/themeService';
R
Rob Lourens 已提交
30
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
31
import { IEditor, IEditorMemento } from 'vs/workbench/common/editor';
32
import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput';
33 34 35 36 37 38
import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout';
import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, 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';
39
import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, EXTENSION_SETTING_TAG, IPreferencesSearchService, ISearchProvider, MODIFIED_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences';
40
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
41
import { IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingsEditorOptions, SettingValueType } from 'vs/workbench/services/preferences/common/preferences';
42
import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
43
import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
R
Rob Lourens 已提交
44

R
Rob Lourens 已提交
45 46 47 48 49 50 51 52 53 54 55 56 57
function createGroupIterator(group: SettingsTreeGroupElement): Iterator<ITreeElement<SettingsTreeGroupChild>> {
	const groupsIt = Iterator.fromArray(group.children);

	return Iterator.map(groupsIt, g => {
		return {
			element: g,
			children: g instanceof SettingsTreeGroupElement ?
				createGroupIterator(g) :
				undefined
		};
	});
}

58 59
const $ = DOM.$;

R
Rob Lourens 已提交
60 61 62 63
interface IFocusEventFromScroll extends KeyboardEvent {
	fromScroll: true;
}

64
const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState';
R
Rob Lourens 已提交
65 66
export class SettingsEditor2 extends BaseEditor {

R
Rob Lourens 已提交
67
	static readonly ID: string = 'workbench.editor.settings2';
68
	private static NUM_INSTANCES: number = 0;
G
Guy Waldman 已提交
69 70
	private static SETTING_UPDATE_FAST_DEBOUNCE: number = 200;
	private static SETTING_UPDATE_SLOW_DEBOUNCE: number = 1000;
R
Rob Lourens 已提交
71

72
	private static readonly SUGGESTIONS: string[] = [
P
Peng Lyu 已提交
73
		`@${MODIFIED_SETTING_TAG}`, '@tag:usesOnlineServices', `@${EXTENSION_SETTING_TAG}`
74 75
	];

76 77 78
	private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean {
		if (isArray(type)) {
			// nullable integer/number or complex
G
Guy Waldman 已提交
79
			return false;
80
		}
R
Rob Lourens 已提交
81
		return type === SettingValueType.Enum ||
P
Pine Wu 已提交
82
			type === SettingValueType.ArrayOfString ||
R
Rob Lourens 已提交
83 84 85
			type === SettingValueType.Complex ||
			type === SettingValueType.Boolean ||
			type === SettingValueType.Exclude;
86 87
	}

88
	private defaultSettingsEditorModel: Settings2EditorModel;
R
Rob Lourens 已提交
89

90
	private rootElement: HTMLElement;
R
Rob Lourens 已提交
91
	private headerContainer: HTMLElement;
92
	private searchWidget: SuggestEnabledInput;
93
	private countElement: HTMLElement;
R
Rob Lourens 已提交
94 95
	private settingsTargetsWidget: SettingsTargetsWidget;

96
	private settingsTreeContainer: HTMLElement;
R
Rob Lourens 已提交
97 98
	private settingsTree: SettingsTree;
	private settingRenderers: SettingTreeRenderers;
99
	private tocTreeModel: TOCTreeModel;
100
	private settingsTreeModel: SettingsTreeModel;
101
	private noResultsMessage: HTMLElement;
102
	private clearFilterLinkContainer: HTMLElement;
R
Rob Lourens 已提交
103

R
Rob Lourens 已提交
104
	private tocTreeContainer: HTMLElement;
R
Rob Lourens 已提交
105
	private tocTree: TOCTree;
R
Rob Lourens 已提交
106

107 108
	private settingsAriaExtraLabelsContainer: HTMLElement;

R
Rob Lourens 已提交
109 110 111
	private delayedFilterLogging: Delayer<void>;
	private localSearchDelayer: Delayer<void>;
	private remoteSearchThrottle: ThrottledDelayer<void>;
R
Rob Lourens 已提交
112
	private searchInProgress: CancellationTokenSource | null;
113

114 115
	private settingFastUpdateDelayer: Delayer<void>;
	private settingSlowUpdateDelayer: Delayer<void>;
R
Rob Lourens 已提交
116
	private pendingSettingUpdate: { key: string, value: any } | null;
R
Rob Lourens 已提交
117

118
	private readonly viewState: ISettingsEditorViewState;
R
Rob Lourens 已提交
119
	private _searchResultModel: SearchResultModel | null;
120

121
	private tocRowFocused: IContextKey<boolean>;
122 123
	private inSettingsEditorContextKey: IContextKey<boolean>;
	private searchFocusContextKey: IContextKey<boolean>;
124

125
	private scheduledRefreshes: Map<string, DOM.IFocusTracker>;
R
Rob Lourens 已提交
126
	private lastFocusedSettingElement: string;
127

128 129 130
	/** Don't spam warnings */
	private hasWarnedMissingSettings: boolean;

131 132
	private editorMemento: IEditorMemento<ISettingsEditor2State>;

R
Rob Lourens 已提交
133
	private tocFocusedElement: SettingsTreeGroupElement | null;
134
	private settingsTreeScrollTop = 0;
135
	private dimension: DOM.Dimension;
136

R
Rob Lourens 已提交
137 138
	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
139
		@IConfigurationService private readonly configurationService: IConfigurationService,
R
Rob Lourens 已提交
140
		@IThemeService themeService: IThemeService,
141 142 143 144
		@IPreferencesService private readonly preferencesService: IPreferencesService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IPreferencesSearchService private readonly preferencesSearchService: IPreferencesSearchService,
		@ILogService private readonly logService: ILogService,
145
		@IContextKeyService contextKeyService: IContextKeyService,
146 147
		@IStorageService private readonly storageService: IStorageService,
		@INotificationService private readonly notificationService: INotificationService,
148
		@IEditorGroupsService protected editorGroupService: IEditorGroupsService,
149
		@IKeybindingService private readonly keybindingService: IKeybindingService
R
Rob Lourens 已提交
150
	) {
151
		super(SettingsEditor2.ID, telemetryService, themeService, storageService);
R
Rob Lourens 已提交
152
		this.delayedFilterLogging = new Delayer<void>(1000);
153
		this.localSearchDelayer = new Delayer(300);
154
		this.remoteSearchThrottle = new ThrottledDelayer(200);
R
Rob Lourens 已提交
155
		this.viewState = { settingsTarget: ConfigurationTarget.USER_LOCAL };
R
Rob Lourens 已提交
156

157 158
		this.settingFastUpdateDelayer = new Delayer<void>(SettingsEditor2.SETTING_UPDATE_FAST_DEBOUNCE);
		this.settingSlowUpdateDelayer = new Delayer<void>(SettingsEditor2.SETTING_UPDATE_SLOW_DEBOUNCE);
159

160 161
		this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);
		this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);
162
		this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService);
163

164 165
		this.scheduledRefreshes = new Map<string, DOM.IFocusTracker>();

B
Benjamin Pasero 已提交
166
		this.editorMemento = this.getEditorMemento<ISettingsEditor2State>(editorGroupService, SETTINGS_EDITOR_STATE_KEY);
167

168
		this._register(configurationService.onDidChangeConfiguration(e => {
169 170 171
			if (e.source !== ConfigurationTarget.DEFAULT) {
				this.onConfigUpdate(e.affectedKeys);
			}
172
		}));
R
Rob Lourens 已提交
173 174
	}

175 176 177 178 179 180 181
	get minimumWidth(): number { return 375; }
	get maximumWidth(): number { return Number.POSITIVE_INFINITY; }

	// these setters need to exist because this extends from BaseEditor
	set minimumWidth(value: number) { /*noop*/ }
	set maximumWidth(value: number) { /*noop*/ }

R
Rob Lourens 已提交
182 183 184 185
	private get currentSettingsModel() {
		return this.searchResultModel || this.settingsTreeModel;
	}

R
Rob Lourens 已提交
186
	private get searchResultModel(): SearchResultModel | null {
187 188 189
		return this._searchResultModel;
	}

R
Rob Lourens 已提交
190
	private set searchResultModel(value: SearchResultModel | null) {
191 192 193 194 195
		this._searchResultModel = value;

		DOM.toggleClass(this.rootElement, 'search-mode', !!this._searchResultModel);
	}

R
Rob Lourens 已提交
196 197 198
	private get currentSettingsContextMenuKeyBindingLabel(): string {
		const keybinding = this.keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU);
		return (keybinding && keybinding.getAriaLabel()) || '';
199 200
	}

R
Rob Lourens 已提交
201
	createEditor(parent: HTMLElement): void {
202
		parent.setAttribute('tabindex', '-1');
203
		this.rootElement = DOM.append(parent, $('.settings-editor'));
R
Rob Lourens 已提交
204

205 206
		this.createHeader(this.rootElement);
		this.createBody(this.rootElement);
207
		this.updateStyles();
R
Rob Lourens 已提交
208 209
	}

R
Rob Lourens 已提交
210
	setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | null, token: CancellationToken): Promise<void> {
211
		this.inSettingsEditorContextKey.set(true);
212
		return super.setInput(input, options, token)
213
			.then(() => timeout(0)) // Force setInput to be async
R
Rob Lourens 已提交
214 215 216
			.then(() => {
				return this.render(token);
			})
R
Rob Lourens 已提交
217
			.then(() => {
R
Rob Lourens 已提交
218 219
				options = options || SettingsEditorOptions.create({});

R
Rob Lourens 已提交
220
				if (!this.viewState.settingsTarget) {
R
Rob Lourens 已提交
221
					if (!options.target) {
R
Rob Lourens 已提交
222
						options.target = ConfigurationTarget.USER_LOCAL;
223
					}
224
				}
R
Rob Lourens 已提交
225

226
				this._setOptions(options);
227

R
Rob Lourens 已提交
228 229 230 231
				this._register(input.onDispose(() => {
					this.searchWidget.setValue('');
				}));

232 233
				// Init TOC selection
				this.updateTreeScrollSync();
234 235

				this.restoreCachedState();
R
Rob Lourens 已提交
236
			});
R
Rob Lourens 已提交
237 238
	}

239
	private restoreCachedState(): void {
R
Rob Lourens 已提交
240
		const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input);
241
		if (cachedState && typeof cachedState.target === 'object') {
242 243 244 245 246 247 248 249 250 251 252
			cachedState.target = URI.revive(cachedState.target);
		}

		if (cachedState) {
			const settingsTarget = cachedState.target;
			this.settingsTargetsWidget.settingsTarget = settingsTarget;
			this.onDidSettingsTargetChange(settingsTarget);
			this.searchWidget.setValue(cachedState.searchQuery);
		}
	}

R
Rob Lourens 已提交
253
	setOptions(options: SettingsEditorOptions | null): void {
254 255
		super.setOptions(options);

R
Rob Lourens 已提交
256 257 258
		if (options) {
			this._setOptions(options);
		}
259
	}
260

261
	private _setOptions(options: SettingsEditorOptions): void {
262 263 264 265
		if (options.query) {
			this.searchWidget.setValue(options.query);
		}

266
		const target: SettingsTarget = options.folderUri || <SettingsTarget>options.target;
R
Rob Lourens 已提交
267 268 269 270
		if (target) {
			this.settingsTargetsWidget.settingsTarget = target;
			this.viewState.settingsTarget = target;
		}
271 272
	}

273 274
	clearInput(): void {
		this.inSettingsEditorContextKey.set(false);
R
Rob Lourens 已提交
275 276 277 278
		if (this.input) {
			this.editorMemento.clearEditorState(this.input, this.group);
		}

279 280 281
		super.clearInput();
	}

R
Rob Lourens 已提交
282
	layout(dimension: DOM.Dimension): void {
283
		this.dimension = dimension;
J
Joao Moreno 已提交
284 285 286 287 288

		if (!this.isVisible()) {
			return;
		}

289 290
		this.layoutTrees(dimension);

291 292
		const innerWidth = Math.min(1000, dimension.width) - 24 * 2; // 24px padding on left and right;
		const monacoWidth = innerWidth - 10 - this.countElement.clientWidth - 12; // minus padding inside inputbox, countElement width, extra padding before countElement
293 294
		this.searchWidget.layout({ height: 20, width: monacoWidth });

295 296
		DOM.toggleClass(this.rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600);
		DOM.toggleClass(this.rootElement, 'narrow-width', dimension.width < 600);
R
Rob Lourens 已提交
297 298 299
	}

	focus(): void {
R
Rob Lourens 已提交
300
		if (this.lastFocusedSettingElement) {
R
Rob Lourens 已提交
301
			const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), this.lastFocusedSettingElement);
R
Rob Lourens 已提交
302
			if (elements.length) {
R
Rob Lourens 已提交
303
				const control = elements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
R
Rob Lourens 已提交
304 305 306 307 308 309 310
				if (control) {
					(<HTMLElement>control).focus();
					return;
				}
			}
		}

311 312 313
		this.focusSearch();
	}

314
	focusSettings(): void {
315 316 317 318 319
		// Update ARIA global labels
		const labelElement = this.settingsAriaExtraLabelsContainer.querySelector('#settings_aria_more_actions_shortcut_label');
		if (labelElement) {
			const settingsContextMenuShortcut = this.currentSettingsContextMenuKeyBindingLabel;
			if (settingsContextMenuShortcut) {
320
				labelElement.setAttribute('aria-label', localize('settingsContextMenuAriaShortcut', "For more actions, Press {0}.", settingsContextMenuShortcut));
321 322 323
			}
		}

R
Rob Lourens 已提交
324
		const firstFocusable = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
325 326
		if (firstFocusable) {
			(<HTMLElement>firstFocusable).focus();
327 328 329
		}
	}

330
	showContextMenu(): void {
R
Rob Lourens 已提交
331 332 333 334 335 336
		const activeElement = this.getActiveElementInSettingsTree();
		if (!activeElement) {
			return;
		}

		const settingDOMElement = this.settingRenderers.getSettingDOMElementForDOMElement(activeElement);
337 338 339 340
		if (!settingDOMElement) {
			return;
		}

R
Rob Lourens 已提交
341
		const focusedKey = this.settingRenderers.getKeyForDOMElementInSetting(settingDOMElement);
342 343 344 345 346 347
		if (!focusedKey) {
			return;
		}

		const elements = this.currentSettingsModel.getElementsByName(focusedKey);
		if (elements && elements[0]) {
R
Rob Lourens 已提交
348
			this.settingRenderers.showContextMenu(elements[0], settingDOMElement);
349 350 351
		}
	}

352
	focusSearch(filter?: string, selectAll = true): void {
353 354 355 356
		if (filter && this.searchWidget) {
			this.searchWidget.setValue(filter);
		}

357
		this.searchWidget.focus(selectAll);
R
Rob Lourens 已提交
358 359
	}

360
	clearSearchResults(): void {
361
		this.searchWidget.setValue('');
362 363
	}

364 365 366 367 368 369 370 371 372 373
	clearSearchFilters(): void {
		let query = this.searchWidget.getValue();

		SettingsEditor2.SUGGESTIONS.forEach(suggestion => {
			query = query.replace(suggestion, '');
		});

		this.searchWidget.setValue(query.trim());
	}

R
Rob Lourens 已提交
374 375 376 377
	private createHeader(parent: HTMLElement): void {
		this.headerContainer = DOM.append(parent, $('.settings-header'));

		const searchContainer = DOM.append(this.headerContainer, $('.search-container'));
378

R
Rob Lourens 已提交
379
		const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings");
380 381 382
		this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, {
			triggerCharacters: ['@'],
			provideResults: (query: string) => {
P
Peng Lyu 已提交
383
				return SettingsEditor2.SUGGESTIONS.filter(tag => query.indexOf(tag) === -1).map(tag => strings.endsWith(tag, ':') ? tag : tag + ' ');
384
			}
385
		}, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, {
386 387 388
				placeholderText: searchBoxLabel,
				focusContextKey: this.searchFocusContextKey,
				// TODO: Aria-live
J
Jeremy Shore 已提交
389 390
			})
		);
391

392 393 394 395
		this._register(this.searchWidget.onFocus(() => {
			this.lastFocusedSettingElement = '';
		}));

396 397 398 399
		this._register(attachSuggestEnabledInputBoxStyler(this.searchWidget, this.themeService, {
			inputBorder: settingsTextInputBorder
		}));

400 401 402 403
		this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget'));
		this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => {
			const background = colors.badgeBackground ? colors.badgeBackground.toString() : null;
			const border = colors.contrastBorder ? colors.contrastBorder.toString() : null;
R
Rob Lourens 已提交
404
			const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : null;
405 406

			this.countElement.style.backgroundColor = background;
R
Rob Lourens 已提交
407
			this.countElement.style.color = foreground;
408 409 410 411 412 413

			this.countElement.style.borderWidth = border ? '1px' : null;
			this.countElement.style.borderStyle = border ? 'solid' : null;
			this.countElement.style.borderColor = border;
		}));

414
		this._register(this.searchWidget.onInputDidChange(() => this.onSearchInputChanged()));
R
Rob Lourens 已提交
415

416
		const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
R
Rob Lourens 已提交
417
		const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
R
Rob Lourens 已提交
418 419
		this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true }));
		this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL;
420
		this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target));
R
Rob Lourens 已提交
421 422
	}

423 424 425
	private onDidSettingsTargetChange(target: SettingsTarget): void {
		this.viewState.settingsTarget = target;

426 427
		// TODO Instead of rebuilding the whole model, refresh and uncache the inspected setting value
		this.onConfigUpdate(undefined, true);
428 429
	}

430
	private onDidClickSetting(evt: ISettingLinkClickEvent, recursed?: boolean): void {
431
		const elements = this.currentSettingsModel.getElementsByName(evt.targetKey);
432
		if (elements && elements[0]) {
433
			let sourceTop = this.settingsTree.getRelativeTop(evt.source);
R
Rob Lourens 已提交
434 435 436 437
			if (typeof sourceTop !== 'number') {
				return;
			}

438 439
			if (sourceTop < 0) {
				// e.g. clicked a searched element, now the search has been cleared
440
				sourceTop = 0.5;
441 442
			}

443
			this.settingsTree.reveal(elements[0], sourceTop);
444

R
Rob Lourens 已提交
445
			const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey);
446
			if (domElements && domElements[0]) {
R
Rob Lourens 已提交
447
				const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
448 449 450 451
				if (control) {
					(<HTMLElement>control).focus();
				}
			}
452 453 454 455 456 457
		} else if (!recursed) {
			const p = this.triggerSearch('');
			p.then(() => {
				this.searchWidget.setValue('');
				this.onDidClickSetting(evt, true);
			});
458 459 460
		}
	}

R
Rob Lourens 已提交
461
	switchToSettingsFile(): Promise<IEditor | null> {
462 463 464 465
		const query = parseQuery(this.searchWidget.getValue());
		return this.openSettingsFile(query.query);
	}

R
Rob Lourens 已提交
466
	private openSettingsFile(query?: string): Promise<IEditor | null> {
467 468
		const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget;

469
		const options: ISettingsEditorOptions = { query };
R
Rob Lourens 已提交
470
		if (currentSettingsTarget === ConfigurationTarget.USER_LOCAL) {
471
			return this.preferencesService.openGlobalSettings(true, options);
R
Rob Lourens 已提交
472 473
		} else if (currentSettingsTarget === ConfigurationTarget.USER_REMOTE) {
			return this.preferencesService.openRemoteSettings();
474
		} else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) {
475
			return this.preferencesService.openWorkspaceSettings(true, options);
476
		} else {
477
			return this.preferencesService.openFolderSettings(currentSettingsTarget, true, options);
478
		}
R
Rob Lourens 已提交
479 480 481 482 483
	}

	private createBody(parent: HTMLElement): void {
		const bodyContainer = DOM.append(parent, $('.settings-body'));

R
Rob Lourens 已提交
484
		this.noResultsMessage = DOM.append(bodyContainer, $('.no-results-message'));
485

486
		this.noResultsMessage.innerText = localize('noResults', "No Settings Found");
487 488 489 490 491

		this.clearFilterLinkContainer = $('span.clear-search-filters');

		this.clearFilterLinkContainer.textContent = ' - ';
		const clearFilterLink = DOM.append(this.clearFilterLinkContainer, $('a.pointer.prominent', { tabindex: 0 }, localize('clearSearchFilters', 'Clear Filters')));
492
		this._register(DOM.addDisposableListener(clearFilterLink, DOM.EventType.CLICK, (e: MouseEvent) => {
493 494 495 496 497 498 499 500 501 502
			DOM.EventHelper.stop(e, false);
			this.clearSearchFilters();
		}));

		DOM.append(this.noResultsMessage, this.clearFilterLinkContainer);

		const clearSearchContainer = $('span.clear-search');
		clearSearchContainer.textContent = ' - ';

		const clearSearch = DOM.append(clearSearchContainer, $('a.pointer.prominent', { tabindex: 0 }, localize('clearSearch', 'Clear Search')));
503
		this._register(DOM.addDisposableListener(clearSearch, DOM.EventType.CLICK, (e: MouseEvent) => {
504 505
			DOM.EventHelper.stop(e, false);
			this.clearSearchResults();
506
			this.focusSearch();
507 508 509 510
		}));

		DOM.append(this.noResultsMessage, clearSearchContainer);

511 512 513 514
		this._register(attachStylerCallback(this.themeService, { editorForeground }, colors => {
			this.noResultsMessage.style.color = colors.editorForeground ? colors.editorForeground.toString() : null;
		}));

R
Rob Lourens 已提交
515 516
		this.createTOC(bodyContainer);

517 518 519 520 521
		this.createFocusSink(
			bodyContainer,
			e => {
				if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) {
					if (this.settingsTree.scrollTop > 0) {
522
						const firstElement = this.settingsTree.firstVisibleElement;
523 524 525 526 527 528 529 530 531 532 533 534 535
						this.settingsTree.reveal(firstElement, 0.1);
						return true;
					}
				} else {
					const firstControl = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR);
					if (firstControl) {
						(<HTMLElement>firstControl).focus();
					}
				}

				return false;
			},
			'settings list focus helper');
R
Rob Lourens 已提交
536

R
Rob Lourens 已提交
537
		this.createSettingsTree(bodyContainer);
R
Rob Lourens 已提交
538

539 540 541 542 543
		this.createFocusSink(
			bodyContainer,
			e => {
				if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) {
					if (this.settingsTree.scrollTop < this.settingsTree.scrollHeight) {
544
						const lastElement = this.settingsTree.lastVisibleElement;
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
						this.settingsTree.reveal(lastElement, 0.9);
						return true;
					}
				}

				return false;
			},
			'settings list focus helper'
		);
	}

	private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement {
		const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink'));
		listFocusSink.setAttribute('aria-label', label);
		listFocusSink.tabIndex = 0;
		this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => {
			if (e.relatedTarget && callback(e)) {
				e.relatedTarget.focus();
			}
		}));

		return listFocusSink;
	}
568

R
Rob Lourens 已提交
569
	private createTOC(parent: HTMLElement): void {
570
		this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState);
R
Rob Lourens 已提交
571 572
		this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container'));

573 574
		this.tocTree = this._register(this.instantiationService.createInstance(TOCTree,
			DOM.append(this.tocTreeContainer, $('.settings-toc-wrapper')),
R
Rob Lourens 已提交
575
			this.viewState));
R
Rob Lourens 已提交
576

577
		this._register(this.tocTree.onDidChangeFocus(e => {
R
Rob Lourens 已提交
578
			const element: SettingsTreeGroupElement | null = e.elements[0];
R
Rob Lourens 已提交
579 580 581
			if (this.tocFocusedElement === element) {
				return;
			}
582

R
Rob Lourens 已提交
583 584 585 586
			this.tocFocusedElement = element;
			this.tocTree.setSelection(element ? [element] : []);
			if (this.searchResultModel) {
				if (this.viewState.filterToCategory !== element) {
587
					this.viewState.filterToCategory = withNullAsUndefined(element);
588 589
					this.renderTree();
					this.settingsTree.scrollTop = 0;
R
Rob Lourens 已提交
590
				}
R
Rob Lourens 已提交
591
			} else if (element && (!e.browserEvent || !(<IFocusEventFromScroll>e.browserEvent).fromScroll)) {
R
Rob Lourens 已提交
592 593
				this.settingsTree.reveal(element, 0);
			}
594 595 596 597 598 599 600 601
		}));

		this._register(this.tocTree.onDidFocus(() => {
			this.tocRowFocused.set(true);
		}));

		this._register(this.tocTree.onDidBlur(() => {
			this.tocRowFocused.set(false);
R
Rob Lourens 已提交
602 603 604 605
		}));
	}

	private createSettingsTree(parent: HTMLElement): void {
606
		this.settingsTreeContainer = DOM.append(parent, $('.settings-tree-container'));
R
Rob Lourens 已提交
607

608 609 610 611 612 613 614 615
		// Add  ARIA extra labels div
		this.settingsAriaExtraLabelsContainer = DOM.append(this.settingsTreeContainer, $('.settings-aria-extra-labels'));
		this.settingsAriaExtraLabelsContainer.id = 'settings_aria_extra_labels';
		// Add global labels here
		const labelDiv = DOM.append(this.settingsAriaExtraLabelsContainer, $('.settings-aria-extra-label'));
		labelDiv.id = 'settings_aria_more_actions_shortcut_label';
		labelDiv.setAttribute('aria-label', '');

R
Rob Lourens 已提交
616 617 618
		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 => {
619
			this.openSettingsFile(settingKey);
620
		}));
R
Rob Lourens 已提交
621 622
		this._register(this.settingRenderers.onDidClickSettingLink(settingName => this.onDidClickSetting(settingName)));
		this._register(this.settingRenderers.onDidFocusSetting(element => {
R
Rob Lourens 已提交
623
			this.lastFocusedSettingElement = element.setting.key;
624 625
			this.settingsTree.reveal(element);
		}));
R
Rob Lourens 已提交
626
		this._register(this.settingRenderers.onDidClickOverrideElement((element: ISettingOverrideClickEvent) => {
627
			if (element.scope.toLowerCase() === 'workspace') {
J
Jeremy Shore 已提交
628
				this.settingsTargetsWidget.updateTarget(ConfigurationTarget.WORKSPACE);
629
			} else if (element.scope.toLowerCase() === 'user') {
R
Rob Lourens 已提交
630
				this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_LOCAL);
631
			} else if (element.scope.toLowerCase() === 'remote') {
R
Rob Lourens 已提交
632
				this.settingsTargetsWidget.updateTarget(ConfigurationTarget.USER_REMOTE);
J
Jeremy Shore 已提交
633 634 635 636
			}

			this.searchWidget.setValue(element.targetKey);
		}));
637

638
		this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree,
639 640
			this.settingsTreeContainer,
			this.viewState,
R
Rob Lourens 已提交
641
			this.settingRenderers.allRenderers));
R
Rob Lourens 已提交
642
		this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex');
643

644
		this._register(this.settingsTree.onDidScroll(() => {
645 646 647 648 649 650 651 652 653 654 655
			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);
656
		}));
657 658
	}

B
Benjamin Pasero 已提交
659 660
	private notifyNoSaveNeeded() {
		if (!this.storageService.getBoolean('hasNotifiedOfSettingsAutosave', StorageScope.GLOBAL, false)) {
B
Benjamin Pasero 已提交
661
			this.storageService.store('hasNotifiedOfSettingsAutosave', true, StorageScope.GLOBAL);
662 663
			this.notificationService.info(localize('settingsNoSaveNeeded', "Your changes are automatically saved as you edit."));
		}
J
Jackson Kearl 已提交
664 665
	}

666
	private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[]): void {
B
Benjamin Pasero 已提交
667
		this.notifyNoSaveNeeded();
668

669 670
		if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) {
			this.updateChangedSetting(key, value);
671 672
		}

673
		this.pendingSettingUpdate = { key, value };
674 675 676 677 678
		if (SettingsEditor2.shouldSettingUpdateFast(type)) {
			this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
		} else {
			this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
		}
679 680
	}

681
	private updateTreeScrollSync(): void {
R
Rob Lourens 已提交
682
		this.settingRenderers.cancelSuggesters();
683 684 685 686
		if (this.searchResultModel) {
			return;
		}

R
Rob Lourens 已提交
687
		if (!this.tocTreeModel) {
688 689
			return;
		}
690

691
		const elementToSync = this.settingsTree.firstVisibleElement;
692 693 694 695
		const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent :
			elementToSync instanceof SettingsTreeGroupElement ? elementToSync :
				null;

R
Rob Lourens 已提交
696 697 698 699 700 701 702 703
		// 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;
		}

704 705 706 707 708 709
		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);
R
Rob Lourens 已提交
710 711 712 713
			if (typeof elementTop !== 'number') {
				return;
			}

714 715 716 717 718 719 720 721 722 723
			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);
R
Rob Lourens 已提交
724

725
			this.tocTree.setSelection([element]);
726

727
			const fakeKeyboardEvent = new KeyboardEvent('keydown');
R
Rob Lourens 已提交
728
			(<IFocusEventFromScroll>fakeKeyboardEvent).fromScroll = true;
729 730 731
			this.tocTree.setFocus([element], fakeKeyboardEvent);
		}
	}
732

733 734 735 736 737 738 739 740 741 742
	private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] {
		const ancestors: any[] = [];

		while (element.parent) {
			if (element.parent.id !== 'root') {
				ancestors.push(element.parent);
			}

			element = element.parent;
		}
743

744
		return ancestors.reverse();
745 746
	}

J
Johannes Rieken 已提交
747
	private updateChangedSetting(key: string, value: any): Promise<void> {
748 749
		// 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
R
Rob Lourens 已提交
750 751
		const settingsTarget = this.settingsTargetsWidget.settingsTarget;
		const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined;
752
		const configurationTarget = <ConfigurationTarget>(resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget);
R
Rob Lourens 已提交
753 754
		const overrides: IConfigurationOverrides = { resource };

755 756
		const isManualReset = value === undefined;

R
Rob Lourens 已提交
757 758 759 760 761 762 763
		// If the user is changing the value back to the default, do a 'reset' instead
		const inspected = this.configurationService.inspect(key, overrides);
		if (inspected.default === value) {
			value = undefined;
		}

		return this.configurationService.updateValue(key, value, overrides, configurationTarget)
764
			.then(() => {
765
				this.renderTree(key, isManualReset);
766 767 768 769 770
				const reportModifiedProps = {
					key,
					query: this.searchWidget.getValue(),
					searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(),
					rawResults: this.searchResultModel && this.searchResultModel.getRawResults(),
771
					showConfiguredOnly: !!this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG),
772 773 774 775 776 777
					isReset: typeof value === 'undefined',
					settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget
				};

				return this.reportModifiedSetting(reportModifiedProps);
			});
778 779
	}

R
Rob Lourens 已提交
780
	private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[] | null, rawResults: ISearchResult[] | null, showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void {
781
		this.pendingSettingUpdate = null;
782

R
Rob Lourens 已提交
783 784 785
		let groupId: string | undefined = undefined;
		let nlpIndex: number | undefined = undefined;
		let displayIndex: number | undefined = undefined;
786
		if (props.searchResults) {
R
Rob Lourens 已提交
787 788 789 790
			const remoteResult = props.searchResults[SearchResultIdx.Remote];
			const localResult = props.searchResults[SearchResultIdx.Local];

			const localIndex = arrays.firstIndex(localResult!.filterMatches, m => m.setting.key === props.key);
791 792 793 794 795 796 797 798
			groupId = localIndex >= 0 ?
				'local' :
				'remote';

			displayIndex = localIndex >= 0 ?
				localIndex :
				remoteResult && (arrays.firstIndex(remoteResult.filterMatches, m => m.setting.key === props.key) + localResult.filterMatches.length);

799 800 801 802 803 804
			if (this.searchResultModel) {
				const rawResults = this.searchResultModel.getRawResults();
				if (rawResults[SearchResultIdx.Remote]) {
					const _nlpIndex = arrays.firstIndex(rawResults[SearchResultIdx.Remote].filterMatches, m => m.setting.key === props.key);
					nlpIndex = _nlpIndex >= 0 ? _nlpIndex : undefined;
				}
805 806 807
			}
		}

R
Rob Lourens 已提交
808 809 810 811
		const reportedTarget = props.settingsTarget === ConfigurationTarget.USER_LOCAL ? 'user' :
			props.settingsTarget === ConfigurationTarget.USER_REMOTE ? 'user_remote' :
				props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' :
					'folder';
812 813 814 815 816 817 818 819 820 821 822 823 824

		const data = {
			key: props.key,
			query: props.query,
			groupId,
			nlpIndex,
			displayIndex,
			showConfiguredOnly: props.showConfiguredOnly,
			isReset: props.isReset,
			target: reportedTarget
		};

		/* __GDPR__
825
			"settingsEditor.settingModified" : {
826 827 828 829 830 831 832 833 834 835
				"key" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
				"query" : { "classification": "CustomerContent", "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" }
			}
		*/
836
		this.telemetryService.publicLog('settingsEditor.settingModified', data);
R
Rob Lourens 已提交
837 838
	}

J
Johannes Rieken 已提交
839
	private render(token: CancellationToken): Promise<any> {
R
Rob Lourens 已提交
840 841
		if (this.input) {
			return this.input.resolve()
842
				.then((model: Settings2EditorModel) => {
843
					if (token.isCancellationRequested) {
R
Rob Lourens 已提交
844
						return undefined;
845 846
					}

847 848
					this._register(model.onDidChangeGroups(() => this.onConfigUpdate()));
					this.defaultSettingsEditorModel = model;
849
					return this.onConfigUpdate();
850
				});
R
Rob Lourens 已提交
851
		}
R
Rob Lourens 已提交
852
		return Promise.resolve(null);
R
Rob Lourens 已提交
853 854
	}

855
	private onSearchModeToggled(): void {
856
		DOM.removeClass(this.rootElement, 'no-toc-search');
857
		if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') {
858
			DOM.toggleClass(this.rootElement, 'no-toc-search', !!this.searchResultModel);
859
		}
860 861
	}

862 863
	private scheduleRefresh(element: HTMLElement, key = ''): void {
		if (key && this.scheduledRefreshes.has(key)) {
864 865 866
			return;
		}

867 868 869 870 871 872 873 874 875 876 877
		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]);
878 879 880
		});
	}

U
Ubuntu 已提交
881
	private onConfigUpdate(keys?: string[], forceRefresh = false): void {
882
		if (keys && this.settingsTreeModel) {
883 884 885
			return this.updateElementsByKey(keys);
		}

886
		const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
887
		const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core');
888 889 890 891 892
		const settingsResult = resolveSettingsTree(tocData, dividedGroups.core);
		const resolvedSettingsRoot = settingsResult.tree;

		// Warn for settings not included in layout
		if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) {
M
Matt Bierner 已提交
893
			const settingKeyList: string[] = [];
894 895 896 897 898 899 900 901
			settingsResult.leftoverSettings.forEach(s => {
				settingKeyList.push(s.key);
			});

			this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`);
			this.hasWarnedMissingSettings = true;
		}

902
		const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core);
R
Rob Lourens 已提交
903
		resolvedSettingsRoot.children!.unshift(commonlyUsed.tree);
904

R
Rob Lourens 已提交
905
		resolvedSettingsRoot.children!.push(resolveExtensionsSettings(dividedGroups.extension || []));
906

907 908 909 910
		if (this.searchResultModel) {
			this.searchResultModel.updateChildren();
		}

911 912
		if (this.settingsTreeModel) {
			this.settingsTreeModel.update(resolvedSettingsRoot);
913

914
			// Make sure that all extensions' settings are included in search results
R
Rob Lourens 已提交
915
			const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input);
916 917 918
			if (cachedState && cachedState.searchQuery) {
				this.triggerSearch(cachedState.searchQuery);
			} else {
919 920
				this.renderTree(undefined, forceRefresh);
				this.refreshTOCTree();
921
			}
922
		} else {
923 924
			this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState);
			this.settingsTreeModel.update(resolvedSettingsRoot);
925
			this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement;
926

R
Rob Lourens 已提交
927
			this.refreshTOCTree();
928 929
			this.refreshTree();

R
Rob Lourens 已提交
930
			this.tocTree.collapseAll();
931 932 933
		}
	}

U
Ubuntu 已提交
934
	private updateElementsByKey(keys: string[]): void {
935 936
		if (keys.length) {
			if (this.searchResultModel) {
R
Rob Lourens 已提交
937
				keys.forEach(key => this.searchResultModel!.updateElementsByName(key));
938 939 940 941 942 943
			}

			if (this.settingsTreeModel) {
				keys.forEach(key => this.settingsTreeModel.updateElementsByName(key));
			}

U
Ubuntu 已提交
944
			keys.forEach(key => this.renderTree(key));
945 946 947 948 949
		} else {
			return this.renderTree();
		}
	}

950 951 952 953 954 955
	private getActiveElementInSettingsTree(): HTMLElement | null {
		return (document.activeElement && DOM.isAncestor(document.activeElement, this.settingsTree.getHTMLElement())) ?
			<HTMLElement>document.activeElement :
			null;
	}

U
Ubuntu 已提交
956
	private renderTree(key?: string, force = false): void {
957
		if (!force && key && this.scheduledRefreshes.has(key)) {
958
			this.updateModifiedLabelForKey(key);
U
Ubuntu 已提交
959
			return;
960 961
		}

962 963
		// If the context view is focused, delay rendering settings
		if (this.contextViewFocused()) {
M
Matt Bierner 已提交
964 965 966 967
			const element = document.querySelector('.context-view');
			if (element) {
				this.scheduleRefresh(element as HTMLElement, key);
			}
U
Ubuntu 已提交
968
			return;
969 970
		}

971
		// If a setting control is currently focused, schedule a refresh for later
R
Rob Lourens 已提交
972 973
		const activeElement = this.getActiveElementInSettingsTree();
		const focusedSetting = activeElement && this.settingRenderers.getSettingDOMElementForDOMElement(activeElement);
974
		if (focusedSetting && !force) {
975 976
			// If a single setting is being refreshed, it's ok to refresh now if that is not the focused setting
			if (key) {
R
Rob Lourens 已提交
977
				const focusedKey = focusedSetting.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR);
P
Pine Wu 已提交
978
				if (focusedKey === key &&
P
Pine Wu 已提交
979 980 981
					// update `list`s live, as they have a separate "submit edit" step built in before this
					(focusedSetting.parentElement && !DOM.hasClass(focusedSetting.parentElement, 'setting-item-list'))
				) {
982

983
					this.updateModifiedLabelForKey(key);
984
					this.scheduleRefresh(focusedSetting, key);
U
Ubuntu 已提交
985
					return;
986 987
				}
			} else {
988
				this.scheduleRefresh(focusedSetting);
U
Ubuntu 已提交
989
				return;
990
			}
991
		}
R
Rob Lourens 已提交
992

R
Rob Lourens 已提交
993 994
		this.renderResultCountMessages();

995
		if (key) {
996
			const elements = this.currentSettingsModel.getElementsByName(key);
997
			if (elements && elements.length) {
998
				// TODO https://github.com/Microsoft/vscode/issues/57360
R
Rob Lourens 已提交
999
				this.refreshTree();
1000 1001
			} else {
				// Refresh requested for a key that we don't know about
U
Ubuntu 已提交
1002
				return;
1003
			}
1004
		} else {
R
Rob Lourens 已提交
1005
			this.refreshTree();
1006 1007
		}

U
Ubuntu 已提交
1008
		return;
R
Rob Lourens 已提交
1009 1010
	}

1011 1012 1013 1014
	private contextViewFocused(): boolean {
		return !!DOM.findParentWithClass(<HTMLElement>document.activeElement, 'context-view');
	}

R
Rob Lourens 已提交
1015
	private refreshTree(): void {
1016 1017 1018
		if (this.isVisible()) {
			this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root));
		}
1019 1020
	}

R
Rob Lourens 已提交
1021
	private refreshTOCTree(): void {
1022
		if (this.isVisible()) {
R
Rob Lourens 已提交
1023
			this.tocTreeModel.update();
1024 1025
			this.tocTree.setChildren(null, createTOCIterator(this.tocTreeModel, this.tocTree));
		}
R
Rob Lourens 已提交
1026 1027
	}

1028
	private updateModifiedLabelForKey(key: string): void {
1029
		const dataElements = this.currentSettingsModel.getElementsByName(key);
1030
		const isModified = dataElements && dataElements[0] && dataElements[0].isConfigured; // all elements are either configured or not
R
Rob Lourens 已提交
1031
		const elements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), key);
1032
		if (elements && elements[0]) {
R
Rob Lourens 已提交
1033
			DOM.toggleClass(elements[0], 'is-configured', !!isModified);
1034 1035 1036
		}
	}

1037
	private onSearchInputChanged(): void {
R
Rob Lourens 已提交
1038 1039
		const query = this.searchWidget.getValue().trim();
		this.delayedFilterLogging.cancel();
1040
		this.triggerSearch(query.replace(/›/g, ' ')).then(() => {
1041
			if (query && this.searchResultModel) {
R
Rob Lourens 已提交
1042
				this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel!.getUniqueResults()));
1043 1044
			}
		});
R
Rob Lourens 已提交
1045 1046
	}

R
Rob Lourens 已提交
1047
	private parseSettingFromJSON(query: string): string | null {
1048 1049 1050 1051
		const match = query.match(/"([a-zA-Z.]+)": /);
		return match && match[1];
	}

J
Johannes Rieken 已提交
1052
	private triggerSearch(query: string): Promise<void> {
1053
		this.viewState.tagFilters = new Set<string>();
P
Peng Lyu 已提交
1054
		this.viewState.extensionFilters = new Set<string>();
1055
		if (query) {
1056
			const parsedQuery = parseQuery(query);
R
Rob Lourens 已提交
1057
			query = parsedQuery.query;
R
Rob Lourens 已提交
1058
			parsedQuery.tags.forEach(tag => this.viewState.tagFilters!.add(tag));
P
Peng Lyu 已提交
1059
			parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId));
1060
		}
1061 1062

		if (query && query !== '@') {
1063
			query = this.parseSettingFromJSON(query) || query;
1064
			return this.triggerFilterPreferences(query);
R
Rob Lourens 已提交
1065
		} else {
P
Peng Lyu 已提交
1066
			if ((this.viewState.tagFilters && this.viewState.tagFilters.size) || (this.viewState.extensionFilters && this.viewState.extensionFilters.size)) {
1067 1068 1069 1070 1071
				this.searchResultModel = this.createFilterModel();
			} else {
				this.searchResultModel = null;
			}

R
Rob Lourens 已提交
1072 1073
			this.localSearchDelayer.cancel();
			this.remoteSearchThrottle.cancel();
1074 1075 1076 1077
			if (this.searchInProgress) {
				this.searchInProgress.cancel();
				this.searchInProgress.dispose();
				this.searchInProgress = null;
R
Rob Lourens 已提交
1078
			}
R
Rob Lourens 已提交
1079

1080
			this.tocTree.setFocus([]);
R
Rob Lourens 已提交
1081
			this.viewState.filterToCategory = undefined;
1082
			this.tocTreeModel.currentSearchModel = this.searchResultModel;
1083
			this.onSearchModeToggled();
1084 1085

			if (this.searchResultModel) {
1086 1087
				// Added a filter model
				this.tocTree.setSelection([]);
R
Rob Lourens 已提交
1088
				this.tocTree.expandAll();
R
Rob Lourens 已提交
1089
				this.renderResultCountMessages();
R
Rob Lourens 已提交
1090
				this.refreshTree();
1091
			} else {
1092
				// Leaving search mode
R
Rob Lourens 已提交
1093
				this.tocTree.collapseAll();
R
Rob Lourens 已提交
1094
				this.renderResultCountMessages();
R
Rob Lourens 已提交
1095
				this.refreshTree();
1096
			}
1097 1098

			this.refreshTOCTree();
R
Rob Lourens 已提交
1099
		}
R
Rob Lourens 已提交
1100

R
Rob Lourens 已提交
1101
		return Promise.resolve();
R
Rob Lourens 已提交
1102 1103
	}

1104 1105 1106 1107 1108 1109 1110 1111 1112
	/**
	 * 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);

		const fullResult: ISearchResult = {
			filterMatches: []
		};
R
Rob Lourens 已提交
1113 1114 1115
		for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) {
			for (const sect of g.sections) {
				for (const setting of sect.settings) {
1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
					fullResult.filterMatches.push({ setting, matches: [], score: 0 });
				}
			}
		}

		filterModel.setResult(0, fullResult);

		return filterModel;
	}

1126 1127 1128 1129
	private reportFilteringUsed(query: string, results: ISearchResult[]): void {
		const nlpResult = results[SearchResultIdx.Remote];
		const nlpMetadata = nlpResult && nlpResult.metadata;

1130 1131 1132
		const durations = {
			nlpResult: nlpMetadata && nlpMetadata.duration
		};
1133 1134

		// Count unique results
1135
		const counts: { nlpResult?: number, filterResult?: number } = {};
1136
		const filterResult = results[SearchResultIdx.Local];
1137 1138 1139 1140
		if (filterResult) {
			counts['filterResult'] = filterResult.filterMatches.length;
		}

1141 1142
		if (nlpResult) {
			counts['nlpResult'] = nlpResult.filterMatches.length;
1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162
		}

		const requestCount = nlpMetadata && nlpMetadata.requestCount;

		const data = {
			query,
			durations,
			counts,
			requestCount
		};

		/* __GDPR__
			"settingsEditor.filter" : {
				"query": { "classification": "CustomerContent", "purpose": "FeatureInsight" },
				"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 }
			}
		*/
1163
		this.telemetryService.publicLog('settingsEditor.filter', data);
1164 1165
	}

J
Johannes Rieken 已提交
1166
	private triggerFilterPreferences(query: string): Promise<void> {
1167 1168 1169 1170 1171 1172 1173 1174 1175 1176
		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 => {
1177
					if (result && !result.exactMatch) {
1178 1179
						this.remoteSearchThrottle.trigger(() => {
							return searchInProgress && !searchInProgress.token.isCancellationRequested ?
R
Rob Lourens 已提交
1180 1181
								this.remoteSearchPreferences(query, this.searchInProgress!.token) :
								Promise.resolve();
1182 1183
						});
					}
1184
				});
1185
			} else {
R
Rob Lourens 已提交
1186
				return Promise.resolve();
1187 1188 1189 1190
			}
		});
	}

R
Rob Lourens 已提交
1191
	private localFilterPreferences(query: string, token?: CancellationToken): Promise<ISearchResult | null> {
1192
		const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
1193
		return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider, token);
R
Rob Lourens 已提交
1194 1195
	}

J
Johannes Rieken 已提交
1196
	private remoteSearchPreferences(query: string, token?: CancellationToken): Promise<void> {
1197
		const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
1198 1199
		const newExtSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query, true);

R
Rob Lourens 已提交
1200
		return Promise.all([
1201 1202
			this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider, token),
			this.filterOrSearchPreferences(query, SearchResultIdx.NewExtensions, newExtSearchProvider, token)
R
Rob Lourens 已提交
1203
		]).then(() => { });
R
Rob Lourens 已提交
1204 1205
	}

R
Rob Lourens 已提交
1206
	private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise<ISearchResult | null> {
1207 1208 1209 1210 1211
		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;
			}
1212

1213 1214 1215 1216
			if (!this.searchResultModel) {
				this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState);
				this.searchResultModel.setResult(type, result);
				this.tocTreeModel.currentSearchModel = this.searchResultModel;
1217
				this.onSearchModeToggled();
1218 1219
			} else {
				this.searchResultModel.setResult(type, result);
1220
				this.tocTreeModel.update();
1221
			}
R
Rob Lourens 已提交
1222

1223
			this.tocTree.setFocus([]);
R
Rob Lourens 已提交
1224
			this.viewState.filterToCategory = undefined;
R
Rob Lourens 已提交
1225
			this.tocTree.expandAll();
1226

U
Ubuntu 已提交
1227
			this.renderTree(undefined, true);
1228
			this.refreshTOCTree();
U
Ubuntu 已提交
1229
			return result;
R
Rob Lourens 已提交
1230 1231 1232
		});
	}

1233
	private renderResultCountMessages() {
1234
		if (!this.currentSettingsModel) {
1235 1236 1237
			return;
		}

R
Rob Lourens 已提交
1238 1239 1240 1241
		this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 0
			? 'initial'
			: 'none';

1242
		if (!this.searchResultModel) {
1243 1244 1245 1246 1247
			if (this.countElement.style.display !== 'none') {
				this.countElement.style.display = 'none';
				this.layout(this.dimension);
			}

R
Rob Lourens 已提交
1248 1249
			DOM.removeClass(this.rootElement, 'no-results');
			return;
1250 1251
		}

1252 1253 1254 1255 1256 1257 1258
		if (this.tocTreeModel && this.tocTreeModel.settingsTreeRoot) {
			const count = this.tocTreeModel.settingsTreeRoot.count;
			switch (count) {
				case 0: this.countElement.innerText = localize('noResults', "No Settings Found"); break;
				case 1: this.countElement.innerText = localize('oneResult', "1 Setting Found"); break;
				default: this.countElement.innerText = localize('moreThanOneResult', "{0} Settings Found", count);
			}
1259

1260 1261 1262 1263
			if (this.countElement.style.display !== 'block') {
				this.countElement.style.display = 'block';
				this.layout(this.dimension);
			}
R
Rob Lourens 已提交
1264
			DOM.toggleClass(this.rootElement, 'no-results', count === 0);
1265
		}
1266 1267
	}

R
Rob Lourens 已提交
1268
	private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider?: ISearchProvider, token?: CancellationToken): Promise<ISearchResult | null> {
R
Rob Lourens 已提交
1269
		const searchP = provider ? provider.searchModel(model, token) : Promise.resolve(null);
R
Rob Lourens 已提交
1270 1271 1272
		return searchP
			.then<ISearchResult>(null, err => {
				if (isPromiseCanceledError(err)) {
R
Rob Lourens 已提交
1273
					return Promise.reject(err);
R
Rob Lourens 已提交
1274 1275
				} else {
					/* __GDPR__
1276
						"settingsEditor.searchError" : {
R
Rob Lourens 已提交
1277 1278 1279 1280 1281 1282 1283
							"message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" },
							"filter": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
						}
					*/
					const message = getErrorMessage(err).trim();
					if (message && message !== 'Error') {
						// "Error" = any generic network error
1284
						this.telemetryService.publicLog('settingsEditor.searchError', { message, filter });
R
Rob Lourens 已提交
1285 1286
						this.logService.info('Setting search error: ' + message);
					}
R
Rob Lourens 已提交
1287
					return Promise.resolve(null);
R
Rob Lourens 已提交
1288 1289 1290 1291
				}
			});
	}

1292
	private layoutTrees(dimension: DOM.Dimension): void {
1293
		const listHeight = dimension.height - (76 + 11 /* header height + padding*/);
1294 1295
		const settingsTreeHeight = listHeight - 14;
		this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`;
1296
		this.settingsTree.layout(settingsTreeHeight, dimension.width);
1297

1298 1299
		const tocTreeHeight = listHeight - 16;
		this.tocTreeContainer.style.height = `${tocTreeHeight}px`;
R
Rob Lourens 已提交
1300
		this.tocTree.layout(tocTreeHeight);
1301
	}
1302

B
Benjamin Pasero 已提交
1303
	protected saveState(): void {
1304 1305 1306
		if (this.isVisible()) {
			const searchQuery = this.searchWidget.getValue().trim();
			const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget;
R
Rob Lourens 已提交
1307 1308 1309
			if (this.group && this.input) {
				this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target });
			}
1310
		}
B
Benjamin Pasero 已提交
1311 1312

		super.saveState();
1313
	}
R
Rob Lourens 已提交
1314
}
1315

1316 1317 1318 1319
interface ISettingsEditor2State {
	searchQuery: string;
	target: SettingsTarget;
}