settingsEditor2.ts 32.5 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';
7
import { Button } from 'vs/base/browser/ui/button/button';
8 9
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { Action } from 'vs/base/common/actions';
10
import * as arrays from 'vs/base/common/arrays';
R
Rob Lourens 已提交
11
import { Delayer, ThrottledDelayer } from 'vs/base/common/async';
R
Rob Lourens 已提交
12
import { CancellationToken } from 'vs/base/common/cancellation';
R
Rob Lourens 已提交
13
import * as collections from 'vs/base/common/collections';
14
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
R
Rob Lourens 已提交
15
import URI from 'vs/base/common/uri';
R
Rob Lourens 已提交
16
import { TPromise } from 'vs/base/common/winjs.base';
17
import { ITree, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree';
18
import { OpenMode, DefaultTreestyler } from 'vs/base/parts/tree/browser/treeDefaults';
R
Rob Lourens 已提交
19 20
import 'vs/css!./media/settingsEditor2';
import { localize } from 'vs/nls';
R
Rob Lourens 已提交
21
import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration';
R
Rob Lourens 已提交
22
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
23
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
R
Rob Lourens 已提交
24
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
R
Rob Lourens 已提交
25
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
26
import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService';
27
import { ILogService } from 'vs/platform/log/common/log';
R
Rob Lourens 已提交
28
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
29
import { attachButtonStyler, attachStyler } from 'vs/platform/theme/common/styler';
30
import { IThemeService } from 'vs/platform/theme/common/themeService';
R
Rob Lourens 已提交
31
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
32
import { EditorOptions, IEditor } from 'vs/workbench/common/editor';
33
import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets';
R
Rob Lourens 已提交
34
import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout';
35
import { ISettingsEditorViewState, resolveExtensionsSettings, resolveSettingsTree, SearchResultIdx, SearchResultModel, SettingsRenderer, SettingsTree, SettingsTreeElement, SettingsTreeFilter, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement, MODIFIED_SETTING_TAG } from 'vs/workbench/parts/preferences/browser/settingsTree';
36
import { TOCDataSource, TOCRenderer, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree';
37
import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_FIRST_ROW_FOCUS, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences';
38
import { IPreferencesService, ISearchResult, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences';
39
import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
R
Rob Lourens 已提交
40
import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
41
import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry';
42
import { settingsHeaderForeground } from 'vs/workbench/parts/preferences/browser/settingsWidgets';
R
Rob Lourens 已提交
43

44 45
const $ = DOM.$;

R
Rob Lourens 已提交
46 47 48 49 50 51
export class SettingsEditor2 extends BaseEditor {

	public static readonly ID: string = 'workbench.editor.settings2';

	private defaultSettingsEditorModel: DefaultSettingsEditorModel;

52
	private rootElement: HTMLElement;
R
Rob Lourens 已提交
53 54 55
	private headerContainer: HTMLElement;
	private searchWidget: SearchWidget;
	private settingsTargetsWidget: SettingsTargetsWidget;
56
	private toolbar: ToolBar;
R
Rob Lourens 已提交
57

58 59
	private settingsTreeContainer: HTMLElement;
	private settingsTree: WorkbenchTree;
60
	private tocTreeModel: TOCTreeModel;
61
	private settingsTreeModel: SettingsTreeModel;
R
Rob Lourens 已提交
62

R
Rob Lourens 已提交
63 64 65
	private tocTreeContainer: HTMLElement;
	private tocTree: WorkbenchTree;

R
Rob Lourens 已提交
66 67 68
	private delayedFilterLogging: Delayer<void>;
	private localSearchDelayer: Delayer<void>;
	private remoteSearchThrottle: ThrottledDelayer<void>;
R
Rob Lourens 已提交
69
	private searchInProgress: TPromise<void>;
R
Rob Lourens 已提交
70

71 72
	private settingUpdateDelayer: Delayer<void>;
	private pendingSettingUpdate: { key: string, value: any };
R
Rob Lourens 已提交
73

74
	private selectedElement: SettingsTreeElement;
75

76
	private viewState: ISettingsEditorViewState;
77
	private searchResultModel: SearchResultModel;
78 79

	private firstRowFocused: IContextKey<boolean>;
80
	private rowFocused: IContextKey<boolean>;
81
	private tocRowFocused: IContextKey<boolean>;
82 83
	private inSettingsEditorContextKey: IContextKey<boolean>;
	private searchFocusContextKey: IContextKey<boolean>;
84

85 86 87
	/** Don't spam warnings */
	private hasWarnedMissingSettings: boolean;

R
Rob Lourens 已提交
88 89 90 91 92
	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
		@IConfigurationService private configurationService: IConfigurationService,
		@IThemeService themeService: IThemeService,
		@IPreferencesService private preferencesService: IPreferencesService,
R
Rob Lourens 已提交
93 94
		@IInstantiationService private instantiationService: IInstantiationService,
		@IPreferencesSearchService private preferencesSearchService: IPreferencesSearchService,
95
		@ILogService private logService: ILogService,
96
		@IEnvironmentService private environmentService: IEnvironmentService,
97 98
		@IContextKeyService contextKeyService: IContextKeyService,
		@IContextMenuService private contextMenuService: IContextMenuService
R
Rob Lourens 已提交
99 100
	) {
		super(SettingsEditor2.ID, telemetryService, themeService);
R
Rob Lourens 已提交
101 102 103
		this.delayedFilterLogging = new Delayer<void>(1000);
		this.localSearchDelayer = new Delayer(100);
		this.remoteSearchThrottle = new ThrottledDelayer(200);
104
		this.viewState = { settingsTarget: ConfigurationTarget.USER };
R
Rob Lourens 已提交
105

106 107
		this.settingUpdateDelayer = new Delayer<void>(500);

108 109
		this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);
		this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);
110
		this.firstRowFocused = CONTEXT_SETTINGS_FIRST_ROW_FOCUS.bindTo(contextKeyService);
111
		this.rowFocused = CONTEXT_SETTINGS_ROW_FOCUS.bindTo(contextKeyService);
112
		this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService);
113

114 115 116 117 118 119 120
		this._register(configurationService.onDidChangeConfiguration(e => {
			this.onConfigUpdate();

			if (e.affectsConfiguration('workbench.settings.tocVisible')) {
				this.updateTOCVisible();
			}
		}));
R
Rob Lourens 已提交
121 122 123
	}

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

127 128
		this.createHeader(this.rootElement);
		this.createBody(this.rootElement);
R
Rob Lourens 已提交
129 130
	}

131
	setInput(input: SettingsEditor2Input, options: EditorOptions, token: CancellationToken): Thenable<void> {
132
		this.inSettingsEditorContextKey.set(true);
133
		return super.setInput(input, options, token)
R
Rob Lourens 已提交
134
			.then(() => {
135
				this.render(token);
R
Rob Lourens 已提交
136 137 138
			});
	}

139 140 141 142 143
	clearInput(): void {
		this.inSettingsEditorContextKey.set(false);
		super.clearInput();
	}

R
Rob Lourens 已提交
144 145
	layout(dimension: DOM.Dimension): void {
		this.searchWidget.layout(dimension);
146 147 148
		this.layoutTrees(dimension);

		DOM.toggleClass(this.rootElement, 'narrow', dimension.width < 600);
R
Rob Lourens 已提交
149 150 151
	}

	focus(): void {
152 153 154
		this.focusSearch();
	}

155 156 157 158 159 160 161 162 163 164 165
	focusSettings(): void {
		const selection = this.settingsTree.getSelection();
		if (selection && selection[0]) {
			this.settingsTree.setFocus(selection[0]);
		} else {
			this.settingsTree.focusFirst();
		}

		this.settingsTree.domFocus();
	}

166
	focusSearch(): void {
R
Rob Lourens 已提交
167 168 169
		this.searchWidget.focus();
	}

170 171 172 173 174 175 176 177
	editSelectedSetting(): void {
		const focus = this.settingsTree.getFocus();
		if (focus instanceof SettingsTreeSettingElement) {
			const itemId = focus.id.replace(/\./g, '_');
			this.focusEditControlForRow(itemId);
		}
	}

178 179 180 181
	clearSearchResults(): void {
		this.searchWidget.clear();
	}

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

185
		const previewHeader = DOM.append(this.headerContainer, $('.settings-preview-header'));
R
Rob Lourens 已提交
186 187 188 189 190

		const previewAlert = DOM.append(previewHeader, $('span.settings-preview-warning'));
		previewAlert.textContent = localize('previewWarning', "Preview");

		const previewTextLabel = DOM.append(previewHeader, $('span.settings-preview-label'));
191
		previewTextLabel.textContent = localize('previewLabel', "This is a preview of our new settings editor");
192

R
Rob Lourens 已提交
193 194 195
		const searchContainer = DOM.append(this.headerContainer, $('.search-container'));
		this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, {
			ariaLabel: localize('SearchSettings.AriaLabel', "Search settings"),
196
			placeholder: localize('SearchSettings.Placeholder', "Search settings"),
S
Sandeep Somavarapu 已提交
197 198
			focusKey: this.searchFocusContextKey,
			ariaLive: 'assertive'
R
Rob Lourens 已提交
199
		}));
200
		this._register(this.searchWidget.onDidChange(() => this.onSearchInputChanged()));
R
Rob Lourens 已提交
201

202
		const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
R
Rob Lourens 已提交
203 204 205
		const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
		this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer));
		this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER;
206 207
		this.settingsTargetsWidget.onDidTargetChange(() => {
			this.viewState.settingsTarget = this.settingsTargetsWidget.settingsTarget;
208
			this.toolbar.context = this.settingsTargetsWidget.settingsTarget;
209 210 211

			this.settingsTreeModel.update();
			this.refreshTreeAndMaintainFocus();
212
		});
R
Rob Lourens 已提交
213

214
		this.createHeaderControls(headerControlsContainer);
R
Rob Lourens 已提交
215 216
	}

217 218 219
	private createHeaderControls(parent: HTMLElement): void {
		const headerControlsContainerRight = DOM.append(parent, $('.settings-header-controls-right'));

220 221 222 223
		this.toolbar = new ToolBar(headerControlsContainerRight, this.contextMenuService, {
			ariaLabel: localize('settingsToolbarLabel', "Settings Editor Actions"),
			actionRunner: this.actionRunner
		});
224

225
		const actions = [
226 227 228 229 230
			this.instantiationService.createInstance(ToggleFilterByTagAction,
				localize('filterModifiedLabel', "Show modified settings only"),
				MODIFIED_SETTING_TAG,
				this,
				this.viewState),
231 232
			this.instantiationService.createInstance(
				ToggleFilterByTagAction,
233
				localize('filterBackgroundOnlineLabel', "Control background online features"),
234 235 236
				'backgroundOnlineFeature',
				this,
				this.viewState),
237 238 239 240
			this.instantiationService.createInstance(OpenSettingsAction)
		];
		this.toolbar.setActions([], actions)();
		this.toolbar.context = this.settingsTargetsWidget.settingsTarget;
241 242
	}

243 244 245 246 247
	private revealSetting(settingName: string): void {
		const element = this.settingsTreeModel.getElementByName(settingName);
		if (element) {
			this.settingsTree.setSelection([element]);
			this.settingsTree.setFocus(element);
248 249
			this.settingsTree.reveal(element, 0);
			this.settingsTree.domFocus();
250 251 252
		}
	}

253 254 255 256 257 258 259 260 261 262
	private openSettingsFile(): TPromise<IEditor> {
		const currentSettingsTarget = this.settingsTargetsWidget.settingsTarget;

		if (currentSettingsTarget === ConfigurationTarget.USER) {
			return this.preferencesService.openGlobalSettings();
		} else if (currentSettingsTarget === ConfigurationTarget.WORKSPACE) {
			return this.preferencesService.openWorkspaceSettings();
		} else {
			return this.preferencesService.openFolderSettings(currentSettingsTarget);
		}
R
Rob Lourens 已提交
263 264 265 266 267
	}

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

268
		this.createTOC(bodyContainer);
R
Rob Lourens 已提交
269
		this.createSettingsTree(bodyContainer);
270 271 272 273

		if (this.environmentService.appQuality !== 'stable') {
			this.createFeedbackButton(bodyContainer);
		}
R
Rob Lourens 已提交
274 275
	}

R
Rob Lourens 已提交
276 277 278
	private createTOC(parent: HTMLElement): void {
		this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container'));

279 280 281
		const tocDataSource = this.instantiationService.createInstance(TOCDataSource);
		const tocRenderer = this.instantiationService.createInstance(TOCRenderer);
		this.tocTreeModel = new TOCTreeModel();
R
Rob Lourens 已提交
282 283 284

		this.tocTree = this.instantiationService.createInstance(WorkbenchTree, this.tocTreeContainer,
			<ITreeConfiguration>{
285 286
				dataSource: tocDataSource,
				renderer: tocRenderer,
287
				controller: this.instantiationService.createInstance(WorkbenchTreeController, { openMode: OpenMode.DOUBLE_CLICK }),
288 289
				filter: this.instantiationService.createInstance(SettingsTreeFilter, this.viewState),
				styler: new DefaultTreestyler(DOM.createStyleSheet(), 'settings-toc-tree'),
R
Rob Lourens 已提交
290 291
			},
			{
292 293
				showLoading: false,
				twistiePixels: 15
R
Rob Lourens 已提交
294
			});
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
		this.tocTree.getHTMLElement().classList.add('settings-toc-tree');

		this._register(attachStyler(this.themeService, {
			listActiveSelectionBackground: editorBackground,
			listActiveSelectionForeground: settingsHeaderForeground,
			listFocusAndSelectionBackground: editorBackground,
			listFocusAndSelectionForeground: settingsHeaderForeground,
			listFocusBackground: editorBackground,
			listFocusForeground: settingsHeaderForeground,
			listHoverForeground: foreground,
			listHoverBackground: editorBackground,
			listInactiveSelectionBackground: editorBackground,
			listInactiveSelectionForeground: settingsHeaderForeground,
		}, colors => {
			this.tocTree.style(colors);
		}));
R
Rob Lourens 已提交
311

312 313
		this._register(this.tocTree.onDidChangeFocus(e => {
			const element = e.focus;
314 315 316 317
			if (this.searchResultModel) {
				this.viewState.filterToCategory = element;
				this.refreshTreeAndMaintainFocus();
			} else if (this.settingsTreeModel) {
R
Rob Lourens 已提交
318
				if (element && !e.payload.fromScroll) {
319 320 321 322
					this.settingsTree.reveal(element, 0);
					this.settingsTree.setSelection([element]);
					this.settingsTree.setFocus(element);
				}
323
			}
324 325 326 327 328 329 330 331
		}));

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

		this._register(this.tocTree.onDidBlur(() => {
			this.tocRowFocused.set(false);
R
Rob Lourens 已提交
332
		}));
333 334 335 336 337 338 339

		this.updateTOCVisible();
	}

	private updateTOCVisible(): void {
		const visible = !!this.configurationService.getValue('workbench.settings.tocVisible');
		DOM.toggleClass(this.tocTreeContainer, 'hidden', !visible);
R
Rob Lourens 已提交
340 341 342
	}

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

345
		const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer);
346
		this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value)));
347
		this._register(renderer.onDidOpenSettings(() => this.openSettingsFile()));
348
		this._register(renderer.onDidClickSettingLink(settingName => this.revealSetting(settingName)));
349

350 351 352
		this.settingsTree = this.instantiationService.createInstance(SettingsTree,
			this.settingsTreeContainer,
			this.viewState,
353
			{
354
				renderer
355
			});
356

357
		this._register(this.settingsTree.onDidChangeFocus(e => {
358
			this.settingsTree.setSelection([e.focus]);
359 360
			if (this.selectedElement) {
				this.settingsTree.refresh(this.selectedElement);
361
			}
362

363 364
			if (e.focus) {
				this.settingsTree.refresh(e.focus);
365
			}
366 367

			this.selectedElement = e.focus;
368 369
		}));

370 371 372 373 374
		this._register(this.settingsTree.onDidBlur(() => {
			this.rowFocused.set(false);
			this.firstRowFocused.set(false);
		}));

375
		this._register(this.settingsTree.onDidChangeSelection(e => {
376
			this.updateTreeScrollSync();
377 378

			let firstRowFocused = false;
379
			let rowFocused = false;
380 381
			const selection: SettingsTreeElement = e.selection[0];
			if (selection) {
382
				rowFocused = true;
383 384 385 386 387 388 389 390
				if (this.searchResultModel) {
					firstRowFocused = selection.id === this.searchResultModel.getChildren()[0].id;
				} else {
					const firstRowId = this.settingsTreeModel.root.children[0] && this.settingsTreeModel.root.children[0].id;
					firstRowFocused = selection.id === firstRowId;
				}
			}

391
			this.rowFocused.set(rowFocused);
392
			this.firstRowFocused.set(firstRowFocused);
393
		}));
394

395 396
		this._register(this.settingsTree.onDidScroll(() => {
			this.updateTreeScrollSync();
397
		}));
398 399
	}

400 401 402 403 404 405 406 407 408 409 410 411
	private createFeedbackButton(parent: HTMLElement): void {
		const feedbackButton = this._register(new Button(parent));
		feedbackButton.label = localize('feedbackButtonLabel', "Provide Feedback");
		feedbackButton.element.classList.add('settings-feedback-button');

		this._register(attachButtonStyler(feedbackButton, this.themeService));
		this._register(feedbackButton.onDidClick(() => {
			// Github master issue
			window.open('https://go.microsoft.com/fwlink/?linkid=2000807');
		}));
	}

412
	toggleFilterByTag(tag: string): TPromise<void> {
413 414
		// Reset other tags, toggle this tag
		const wasFiltered = this.viewState.tagFilters && this.viewState.tagFilters.has(tag);
415
		const isFiltered = !wasFiltered;
416
		this.viewState.tagFilters = new Set<string>();
417 418 419 420 421 422 423 424 425 426 427
		if (isFiltered) {
			this.viewState.tagFilters.add(tag);
		}

		DOM.toggleClass(this.rootElement, 'settings-filtered-by-tag', isFiltered);
		return this.refreshTreeAndMaintainFocus().then(() => {
			this.settingsTree.setScrollPosition(0);
			this.expandAll(this.settingsTree);
		});
	}

R
Rob Lourens 已提交
428
	private onDidChangeSetting(key: string, value: any): void {
429 430
		if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) {
			this.updateChangedSetting(key, value);
431 432
		}

433 434 435 436
		this.pendingSettingUpdate = { key, value };
		this.settingUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
	}

437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
	private updateTreeScrollSync(): void {
		if (this.searchResultModel) {
			return;
		}

		if (!this.tocTree.getInput()) {
			return;
		}

		let elementToSync = this.settingsTree.getFirstVisibleElement();
		const selection = this.settingsTree.getSelection()[0];
		if (selection) {
			const selectionPos = this.settingsTree.getRelativeTop(selection);
			if (selectionPos >= 0 && selectionPos <= 1) {
				elementToSync = selection;
			}
		}

		const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent :
			elementToSync instanceof SettingsTreeGroupElement ? elementToSync :
				null;

		if (element && this.tocTree.getSelection()[0] !== element) {
			const elementTop = this.tocTree.getRelativeTop(element);
			if (elementTop < 0) {
				this.tocTree.reveal(element, 0);
			} else if (elementTop > 1) {
				this.tocTree.reveal(element, 1);
			}

467 468
			this.tocTree.setSelection([element]);
			this.tocTree.setFocus(element, { fromScroll: true });
469 470 471
		}
	}

472 473 474
	private updateChangedSetting(key: string, value: any): TPromise<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
R
Rob Lourens 已提交
475 476 477 478 479 480 481 482 483 484 485 486
		const settingsTarget = this.settingsTargetsWidget.settingsTarget;
		const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined;
		const configurationTarget = <ConfigurationTarget>(resource ? undefined : settingsTarget);
		const overrides: IConfigurationOverrides = { resource };

		// 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)
487 488 489 490 491 492 493
			.then(() => this.refreshTreeAndMaintainFocus())
			.then(() => {
				const reportModifiedProps = {
					key,
					query: this.searchWidget.getValue(),
					searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(),
					rawResults: this.searchResultModel && this.searchResultModel.getRawResults(),
494
					showConfiguredOnly: this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG),
495 496 497 498 499 500
					isReset: typeof value === 'undefined',
					settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget
				};

				return this.reportModifiedSetting(reportModifiedProps);
			});
501 502 503
	}

	private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[], rawResults: ISearchResult[], showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void {
504
		this.pendingSettingUpdate = null;
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521

		const remoteResult = props.searchResults && props.searchResults[SearchResultIdx.Remote];
		const localResult = props.searchResults && props.searchResults[SearchResultIdx.Local];

		let groupId = undefined;
		let nlpIndex = undefined;
		let displayIndex = undefined;
		if (props.searchResults) {
			const localIndex = arrays.firstIndex(localResult.filterMatches, m => m.setting.key === props.key);
			groupId = localIndex >= 0 ?
				'local' :
				'remote';

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

522 523 524 525 526 527
			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;
				}
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
			}
		}

		const reportedTarget = props.settingsTarget === ConfigurationTarget.USER ? 'user' :
			props.settingsTarget === ConfigurationTarget.WORKSPACE ? 'workspace' :
				'folder';

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

		/* __GDPR__
547
			"settingsEditor.settingModified" : {
548 549 550 551 552 553 554 555 556 557
				"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" }
			}
		*/
558
		this.telemetryService.publicLog('settingsEditor.settingModified', data);
R
Rob Lourens 已提交
559 560
	}

561
	private render(token: CancellationToken): TPromise<any> {
R
Rob Lourens 已提交
562 563
		if (this.input) {
			return this.input.resolve()
564 565 566 567 568 569
				.then((model: DefaultSettingsEditorModel) => {
					if (token.isCancellationRequested) {
						return void 0;
					}

					this.defaultSettingsEditorModel = model;
570
					this.onConfigUpdate();
571
				});
R
Rob Lourens 已提交
572 573 574 575
		}
		return TPromise.as(null);
	}

576
	private toggleSearchMode(): void {
577 578 579 580
		DOM.removeClass(this.rootElement, 'search-mode');
		if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') {
			DOM.toggleClass(this.rootElement, 'search-mode', !!this.searchResultModel);
		}
581 582
	}

583
	private onConfigUpdate(): TPromise<void> {
584
		const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
585
		const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core');
586 587 588 589 590 591 592 593 594 595 596 597 598 599
		const settingsResult = resolveSettingsTree(tocData, dividedGroups.core);
		const resolvedSettingsRoot = settingsResult.tree;

		// Warn for settings not included in layout
		if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) {
			let settingKeyList = [];
			settingsResult.leftoverSettings.forEach(s => {
				settingKeyList.push(s.key);
			});

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

600
		const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core);
601
		resolvedSettingsRoot.children.unshift(commonlyUsed.tree);
602

603 604
		resolvedSettingsRoot.children.push(resolveExtensionsSettings(dividedGroups.extension || []));

605 606 607 608
		if (this.searchResultModel) {
			this.searchResultModel.updateChildren();
		}

609 610 611 612
		if (this.settingsTreeModel) {
			this.settingsTreeModel.update(resolvedSettingsRoot);
		} else {
			this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, resolvedSettingsRoot);
613
			this.settingsTree.setInput(this.settingsTreeModel.root);
614 615 616 617 618 619 620

			this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement;
			if (this.tocTree.getInput()) {
				this.tocTree.refresh();
			} else {
				this.tocTree.setInput(this.tocTreeModel);
			}
621 622 623 624 625
		}

		return this.refreshTreeAndMaintainFocus();
	}

R
Rob Lourens 已提交
626 627 628 629 630 631 632 633
	private refreshTreeAndMaintainFocus(): TPromise<any> {
		// Sort of a hack to maintain focus on the focused control across a refresh
		const focusedRowItem = DOM.findParentWithClass(<HTMLElement>document.activeElement, 'setting-item');
		const focusedRowId = focusedRowItem && focusedRowItem.id;
		const selection = focusedRowId && document.activeElement.tagName.toLowerCase() === 'input' ?
			(<HTMLInputElement>document.activeElement).selectionStart :
			null;

634 635 636
		return this.settingsTree.refresh()
			.then(() => {
				if (focusedRowId) {
637
					this.focusEditControlForRow(focusedRowId, selection);
R
Rob Lourens 已提交
638
				}
639 640 641 642
			})
			.then(() => {
				return this.tocTree.refresh();
			});
643 644
	}

645 646 647 648 649 650 651 652 653 654 655
	private focusEditControlForRow(id: string, selection?: number): void {
		const rowSelector = `.setting-item#${id}`;
		const inputElementToFocus: HTMLElement = this.settingsTreeContainer.querySelector(`${rowSelector} input, ${rowSelector} select, ${rowSelector} a, ${rowSelector} .monaco-custom-checkbox`);
		if (inputElementToFocus) {
			inputElementToFocus.focus();
			if (typeof selection === 'number') {
				(<HTMLInputElement>inputElementToFocus).setSelectionRange(selection, selection);
			}
		}
	}

656
	private onSearchInputChanged(): void {
R
Rob Lourens 已提交
657 658
		const query = this.searchWidget.getValue().trim();
		this.delayedFilterLogging.cancel();
659
		this.triggerSearch(query).then(() => {
660
			if (query && this.searchResultModel) {
661
				this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel.getUniqueResults()));
662 663
			}
		});
R
Rob Lourens 已提交
664 665 666 667
	}

	private triggerSearch(query: string): TPromise<void> {
		if (query) {
R
Rob Lourens 已提交
668
			return this.searchInProgress = TPromise.join([
R
Rob Lourens 已提交
669
				this.localSearchDelayer.trigger(() => this.localFilterPreferences(query)),
670
				this.remoteSearchThrottle.trigger(() => this.remoteSearchPreferences(query), 500)
R
Rob Lourens 已提交
671 672 673
			]).then(() => {
				this.searchInProgress = null;
			});
R
Rob Lourens 已提交
674 675 676
		} else {
			this.localSearchDelayer.cancel();
			this.remoteSearchThrottle.cancel();
R
Rob Lourens 已提交
677 678 679
			if (this.searchInProgress && this.searchInProgress.cancel) {
				this.searchInProgress.cancel();
			}
R
Rob Lourens 已提交
680

681
			this.searchResultModel = null;
682
			this.tocTreeModel.currentSearchModel = null;
683
			this.viewState.filterToCategory = null;
684
			this.tocTree.refresh();
685
			this.toggleSearchMode();
686
			this.settingsTree.setInput(this.settingsTreeModel.root);
687

R
Rob Lourens 已提交
688 689 690 691
			return TPromise.wrap(null);
		}
	}

R
Rob Lourens 已提交
692 693 694 695 696 697
	private expandAll(tree: ITree): void {
		const nav = tree.getNavigator();
		let cur;
		while (cur = nav.next()) {
			tree.expand(cur);
		}
698 699
	}

700 701 702 703 704 705 706 707 708 709
	private reportFilteringUsed(query: string, results: ISearchResult[]): void {
		const nlpResult = results[SearchResultIdx.Remote];
		const nlpMetadata = nlpResult && nlpResult.metadata;

		const durations = {};
		durations['nlpResult'] = nlpMetadata && nlpMetadata.duration;

		// Count unique results
		const counts = {};
		const filterResult = results[SearchResultIdx.Local];
710 711 712 713
		if (filterResult) {
			counts['filterResult'] = filterResult.filterMatches.length;
		}

714 715
		if (nlpResult) {
			counts['nlpResult'] = nlpResult.filterMatches.length;
716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
		}

		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 }
			}
		*/
736
		this.telemetryService.publicLog('settingsEditor.filter', data);
737 738
	}

R
Rob Lourens 已提交
739
	private localFilterPreferences(query: string): TPromise<void> {
740 741
		const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider);
R
Rob Lourens 已提交
742 743 744
	}

	private remoteSearchPreferences(query: string): TPromise<void> {
745 746
		const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider);
R
Rob Lourens 已提交
747 748 749 750 751
	}

	private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider: ISearchProvider): TPromise<void> {
		const filterPs: TPromise<ISearchResult>[] = [this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider)];

R
Rob Lourens 已提交
752 753 754 755 756 757 758
		let isCanceled = false;
		return new TPromise(resolve => {
			return TPromise.join(filterPs).then(results => {
				if (isCanceled) {
					// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
					return null;
				}
759

R
Rob Lourens 已提交
760 761
				const [result] = results;
				if (!this.searchResultModel) {
762
					this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState);
763
					this.searchResultModel.setResult(type, result);
764
					this.tocTreeModel.currentSearchModel = this.searchResultModel;
765
					this.toggleSearchMode();
R
Rob Lourens 已提交
766
					this.settingsTree.setInput(this.searchResultModel);
767 768
				} else {
					this.searchResultModel.setResult(type, result);
R
Rob Lourens 已提交
769 770
				}

771
				this.tocTreeModel.update();
R
Rob Lourens 已提交
772 773 774 775
				resolve(this.refreshTreeAndMaintainFocus());
			});
		}, () => {
			isCanceled = true;
R
Rob Lourens 已提交
776 777 778 779 780 781 782 783 784 785 786
		});
	}

	private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider: ISearchProvider): TPromise<ISearchResult> {
		const searchP = provider ? provider.searchModel(model) : TPromise.wrap(null);
		return searchP
			.then<ISearchResult>(null, err => {
				if (isPromiseCanceledError(err)) {
					return TPromise.wrapError(err);
				} else {
					/* __GDPR__
787
						"settingsEditor.searchError" : {
R
Rob Lourens 已提交
788 789 790 791 792 793 794
							"message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" },
							"filter": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
						}
					*/
					const message = getErrorMessage(err).trim();
					if (message && message !== 'Error') {
						// "Error" = any generic network error
795
						this.telemetryService.publicLog('settingsEditor.searchError', { message, filter });
R
Rob Lourens 已提交
796 797 798 799 800 801 802
						this.logService.info('Setting search error: ' + message);
					}
					return null;
				}
			});
	}

803
	private layoutTrees(dimension: DOM.Dimension): void {
804
		const listHeight = dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 11 /*padding*/);
805 806
		this.settingsTreeContainer.style.height = `${listHeight}px`;
		this.settingsTree.layout(listHeight, 800);
807

808 809 810 811 812
		const selectedSetting = this.settingsTree.getSelection()[0];
		if (selectedSetting) {
			this.settingsTree.refresh(selectedSetting);
		}

813 814
		this.tocTreeContainer.style.height = `${listHeight}px`;
		this.tocTree.layout(listHeight, 175);
815
	}
R
Rob Lourens 已提交
816
}
817 818 819

class OpenSettingsAction extends Action {
	static readonly ID = 'settings.openSettingsJson';
820
	static readonly LABEL = localize('openSettingsJsonLabel', "Open settings.json");
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846

	constructor(
		@IPreferencesService private readonly preferencesService: IPreferencesService,
	) {
		super(OpenSettingsAction.ID, OpenSettingsAction.LABEL, 'open-settings-json');
	}


	run(context?: SettingsTarget): TPromise<void> {
		return this._run(context)
			.then(() => { });
	}

	private _run(context?: SettingsTarget): TPromise<any> {
		if (context === ConfigurationTarget.USER) {
			return this.preferencesService.openGlobalSettings();
		} else if (context === ConfigurationTarget.WORKSPACE) {
			return this.preferencesService.openWorkspaceSettings();
		} else if (URI.isUri(context)) {
			return this.preferencesService.openFolderSettings(context);
		}

		return TPromise.wrap(null);
	}
}

847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
class ToggleFilterByTagAction extends Action {
	static readonly ID = 'settings.toggleFilterByTag';

	get checked(): boolean {
		return this.viewState.tagFilters && this.viewState.tagFilters.has(this.tag);
	}

	constructor(
		label: string,
		private tag: string,
		private settingsEditor: SettingsEditor2,
		private viewState: ISettingsEditorViewState
	) {
		super(ToggleFilterByTagAction.ID, label, 'toggle-filter-tag');
	}

	run(): TPromise<void> {
		return this.settingsEditor.toggleFilterByTag(this.tag);
	}
}