settingsEditor2.ts 33.0 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
import * as arrays from 'vs/base/common/arrays';
R
Rob Lourens 已提交
9
import { Delayer, ThrottledDelayer } from 'vs/base/common/async';
R
Rob Lourens 已提交
10
import { CancellationToken } from 'vs/base/common/cancellation';
R
Rob Lourens 已提交
11
import * as collections from 'vs/base/common/collections';
R
Rob Lourens 已提交
12
import { Color, RGBA } from 'vs/base/common/color';
13
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
R
Rob Lourens 已提交
14
import URI from 'vs/base/common/uri';
R
Rob Lourens 已提交
15
import { TPromise } from 'vs/base/common/winjs.base';
16
import { ITree, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree';
17
import { DefaultTreestyler, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults';
R
Rob Lourens 已提交
18 19
import 'vs/css!./media/settingsEditor2';
import { localize } from 'vs/nls';
R
Rob Lourens 已提交
20
import { ConfigurationTarget, IConfigurationOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration';
R
Rob Lourens 已提交
21 22
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
R
Rob Lourens 已提交
23
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
24
import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService';
25
import { ILogService } from 'vs/platform/log/common/log';
R
Rob Lourens 已提交
26
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
27
import { editorBackground, focusBorder, foreground, listInactiveSelectionBackground, registerColor } from 'vs/platform/theme/common/colorRegistry';
28 29
import { attachButtonStyler, attachStyler } from 'vs/platform/theme/common/styler';
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
R
Rob Lourens 已提交
30
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
31
import { EditorOptions, IEditor } from 'vs/workbench/common/editor';
32
import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets';
R
Rob Lourens 已提交
33 34
import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout';
import { ISettingsEditorViewState, NonExpandableTree, resolveExtensionsSettings, resolveSettingsTree, SearchResultIdx, SearchResultModel, SettingsAccessibilityProvider, SettingsDataSource, SettingsRenderer, SettingsTreeController, SettingsTreeElement, SettingsTreeFilter, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTree';
35
import { TOCDataSource, TOCRenderer, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree';
36
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';
37
import { IPreferencesService, ISearchResult, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences';
38
import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
R
Rob Lourens 已提交
39
import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
R
Rob Lourens 已提交
40

41 42
const $ = DOM.$;

43 44 45 46 47 48
export const settingItemInactiveSelectionBorder = registerColor('settings.inactiveSelectedItemBorder', {
	dark: '#3F3F46',
	light: '#CCCEDB',
	hc: null
}, localize('settingItemInactiveSelectionBorder', "The color of the selected setting row border, when the settings list does not have focus."));

R
Rob Lourens 已提交
49 50 51 52 53 54
export class SettingsEditor2 extends BaseEditor {

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

	private defaultSettingsEditorModel: DefaultSettingsEditorModel;

55
	private rootElement: HTMLElement;
R
Rob Lourens 已提交
56 57 58 59
	private headerContainer: HTMLElement;
	private searchWidget: SearchWidget;
	private settingsTargetsWidget: SettingsTargetsWidget;

60
	private showConfiguredSettingsOnlyCheckbox: HTMLInputElement;
R
Rob Lourens 已提交
61

62 63 64
	private settingsTreeContainer: HTMLElement;
	private settingsTree: WorkbenchTree;
	private treeDataSource: SettingsDataSource;
65
	private tocTreeModel: TOCTreeModel;
66
	private settingsTreeModel: SettingsTreeModel;
R
Rob Lourens 已提交
67

R
Rob Lourens 已提交
68 69 70
	private tocTreeContainer: HTMLElement;
	private tocTree: WorkbenchTree;

R
Rob Lourens 已提交
71 72 73
	private delayedFilterLogging: Delayer<void>;
	private localSearchDelayer: Delayer<void>;
	private remoteSearchThrottle: ThrottledDelayer<void>;
R
Rob Lourens 已提交
74
	private searchInProgress: TPromise<void>;
R
Rob Lourens 已提交
75

76 77
	private settingUpdateDelayer: Delayer<void>;
	private pendingSettingUpdate: { key: string, value: any };
R
Rob Lourens 已提交
78

79
	private selectedElement: SettingsTreeElement;
80

81
	private viewState: ISettingsEditorViewState;
82
	private searchResultModel: SearchResultModel;
83 84

	private firstRowFocused: IContextKey<boolean>;
85
	private rowFocused: IContextKey<boolean>;
86
	private tocRowFocused: IContextKey<boolean>;
87 88
	private inSettingsEditorContextKey: IContextKey<boolean>;
	private searchFocusContextKey: IContextKey<boolean>;
89

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

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

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

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

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

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

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

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

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

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

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

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

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

		this.settingsTree.domFocus();
	}

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

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

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

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

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

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

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

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

203 204 205 206 207 208 209 210 211 212 213 214 215 216
		const advancedCustomization = DOM.append(this.headerContainer, $('.settings-advanced-customization'));
		const advancedCustomizationLabel = DOM.append(advancedCustomization, $('span.settings-advanced-customization-label'));
		advancedCustomizationLabel.textContent = localize('advancedCustomizationLabel', "For advanced customizations open and edit") + ' ';
		const openSettingsButton = this._register(new Button(advancedCustomization, { title: true, buttonBackground: null, buttonHoverBackground: null }));
		this._register(attachButtonStyler(openSettingsButton, this.themeService, {
			buttonBackground: Color.transparent.toString(),
			buttonHoverBackground: Color.transparent.toString(),
			buttonForeground: foreground
		}));
		openSettingsButton.label = localize('openSettingsLabel', "settings.json");
		openSettingsButton.element.classList.add('open-settings-button');

		this._register(openSettingsButton.onDidClick(() => this.openSettingsFile()));

217
		const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
R
Rob Lourens 已提交
218 219 220
		const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
		this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer));
		this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER;
221 222
		this.settingsTargetsWidget.onDidTargetChange(() => {
			this.viewState.settingsTarget = this.settingsTargetsWidget.settingsTarget;
223 224 225

			this.settingsTreeModel.update();
			this.refreshTreeAndMaintainFocus();
226
		});
R
Rob Lourens 已提交
227

228
		this.createHeaderControls(headerControlsContainer);
R
Rob Lourens 已提交
229 230
	}

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

234 235 236
		this.showConfiguredSettingsOnlyCheckbox = DOM.append(headerControlsContainerRight, $('input#configured-only-checkbox'));
		this.showConfiguredSettingsOnlyCheckbox.type = 'checkbox';
		const showConfiguredSettingsOnlyLabel = <HTMLLabelElement>DOM.append(headerControlsContainerRight, $('label.configured-only-label'));
237
		showConfiguredSettingsOnlyLabel.textContent = localize('showOverriddenOnly', "Show modified only");
238
		showConfiguredSettingsOnlyLabel.htmlFor = 'configured-only-checkbox';
239

240
		this._register(DOM.addDisposableListener(this.showConfiguredSettingsOnlyCheckbox, 'change', e => this.onShowConfiguredOnlyClicked()));
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
				filter: this.instantiationService.createInstance(SettingsTreeFilter, this.viewState)
R
Rob Lourens 已提交
289 290
			},
			{
291 292
				showLoading: false,
				twistiePixels: 15
R
Rob Lourens 已提交
293 294
			});

295 296
		this._register(this.tocTree.onDidChangeFocus(e => {
			const element = e.focus;
297 298 299 300
			if (this.searchResultModel) {
				this.viewState.filterToCategory = element;
				this.refreshTreeAndMaintainFocus();
			} else if (this.settingsTreeModel) {
R
Rob Lourens 已提交
301
				if (element && !e.payload.fromScroll) {
302 303 304 305
					this.settingsTree.reveal(element, 0);
					this.settingsTree.setSelection([element]);
					this.settingsTree.setFocus(element);
				}
306
			}
307 308 309 310 311 312 313 314 315

		}));

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

		this._register(this.tocTree.onDidBlur(() => {
			this.tocRowFocused.set(false);
R
Rob Lourens 已提交
316
		}));
317 318 319 320 321 322 323

		this.updateTOCVisible();
	}

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

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

329
		this.treeDataSource = this.instantiationService.createInstance(SettingsDataSource, this.viewState);
330
		const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer);
331
		this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value)));
332
		this._register(renderer.onDidOpenSettings(() => this.openSettingsFile()));
333
		this._register(renderer.onDidClickSettingLink(settingName => this.revealSetting(settingName)));
334

335
		const treeClass = 'settings-editor-tree';
336
		this.settingsTree = this.instantiationService.createInstance(NonExpandableTree, this.settingsTreeContainer,
337
			<ITreeConfiguration>{
338
				dataSource: this.treeDataSource,
R
Rob Lourens 已提交
339
				renderer,
340 341
				controller: this.instantiationService.createInstance(SettingsTreeController),
				accessibilityProvider: this.instantiationService.createInstance(SettingsAccessibilityProvider),
342 343
				filter: this.instantiationService.createInstance(SettingsTreeFilter, this.viewState),
				styler: new DefaultTreestyler(DOM.createStyleSheet(), treeClass)
344 345 346 347
			},
			{
				ariaLabel: localize('treeAriaLabel', "Settings"),
				showLoading: false,
348 349
				indentPixels: 0,
				twistiePixels: 0,
350
			});
351

352
		this._register(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
353
			const activeBorderColor = theme.getColor(focusBorder);
354
			if (activeBorderColor) {
355
				collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree:focus .monaco-tree-row.focused {outline: solid 1px ${activeBorderColor}; outline-offset: -1px; }`);
356 357
			}

358
			const inactiveBorderColor = theme.getColor(settingItemInactiveSelectionBorder);
359
			if (inactiveBorderColor) {
360
				collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree .monaco-tree-row.focused {outline: solid 1px ${inactiveBorderColor}; outline-offset: -1px; }`);
361
			}
R
Rob Lourens 已提交
362 363 364 365 366 367 368 369

			const foregroundColor = theme.getColor(foreground);
			if (foregroundColor) {
				// Links appear inside other elements in markdown. CSS opacity acts like a mask. So we have to dynamically compute the description color to avoid
				// applying an opacity to the link color.
				const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, .7));
				collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description { color: ${fgWithOpacity}; }`);
			}
370 371
		}));

372 373
		this.settingsTree.getHTMLElement().classList.add(treeClass);

374
		this._register(attachStyler(this.themeService, {
375 376 377 378 379 380 381 382 383 384
			listActiveSelectionBackground: editorBackground,
			listActiveSelectionForeground: foreground,
			listFocusAndSelectionBackground: editorBackground,
			listFocusAndSelectionForeground: foreground,
			listFocusBackground: editorBackground,
			listFocusForeground: foreground,
			listHoverForeground: foreground,
			listHoverBackground: editorBackground,
			listInactiveSelectionBackground: editorBackground,
			listInactiveSelectionForeground: foreground
385 386
		}, colors => {
			this.settingsTree.style(colors);
387
		}));
388

389
		this._register(this.settingsTree.onDidChangeFocus(e => {
390
			this.settingsTree.setSelection([e.focus]);
391 392
			if (this.selectedElement) {
				this.settingsTree.refresh(this.selectedElement);
393
			}
394

395 396
			if (e.focus) {
				this.settingsTree.refresh(e.focus);
397
			}
398 399

			this.selectedElement = e.focus;
400 401
		}));

402 403 404 405 406
		this._register(this.settingsTree.onDidBlur(() => {
			this.rowFocused.set(false);
			this.firstRowFocused.set(false);
		}));

407
		this._register(this.settingsTree.onDidChangeSelection(e => {
408
			this.updateTreeScrollSync();
409 410

			let firstRowFocused = false;
411
			let rowFocused = false;
412 413
			const selection: SettingsTreeElement = e.selection[0];
			if (selection) {
414
				rowFocused = true;
415 416 417 418 419 420 421 422
				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;
				}
			}

423
			this.rowFocused.set(rowFocused);
424
			this.firstRowFocused.set(firstRowFocused);
425
		}));
426

427 428
		this._register(this.settingsTree.onDidScroll(() => {
			this.updateTreeScrollSync();
429
		}));
430 431
	}

432 433 434 435 436 437 438 439 440 441 442 443
	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');
		}));
	}

444
	private onShowConfiguredOnlyClicked(): void {
445
		this.viewState.showConfiguredOnly = this.showConfiguredSettingsOnlyCheckbox.checked;
R
Rob Lourens 已提交
446
		this.refreshTreeAndMaintainFocus();
447 448
		this.tocTree.refresh();
		this.settingsTree.setScrollPosition(0);
449
		this.expandAll(this.settingsTree);
450 451
	}

R
Rob Lourens 已提交
452
	private onDidChangeSetting(key: string, value: any): void {
453 454
		if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) {
			this.updateChangedSetting(key, value);
455 456
		}

457 458 459 460
		this.pendingSettingUpdate = { key, value };
		this.settingUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
	}

461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
	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);
			}

491 492
			this.tocTree.setSelection([element]);
			this.tocTree.setFocus(element, { fromScroll: true });
493 494 495
		}
	}

496 497 498
	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 已提交
499 500 501 502 503 504 505 506 507 508 509 510
		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)
511 512 513 514 515 516 517 518 519 520 521 522 523 524
			.then(() => this.refreshTreeAndMaintainFocus())
			.then(() => {
				const reportModifiedProps = {
					key,
					query: this.searchWidget.getValue(),
					searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(),
					rawResults: this.searchResultModel && this.searchResultModel.getRawResults(),
					showConfiguredOnly: this.viewState.showConfiguredOnly,
					isReset: typeof value === 'undefined',
					settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget
				};

				return this.reportModifiedSetting(reportModifiedProps);
			});
525 526 527
	}

	private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[], rawResults: ISearchResult[], showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void {
528
		this.pendingSettingUpdate = null;
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545

		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);

546 547 548 549 550 551
			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;
				}
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
			}
		}

		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__
571
			"settingsEditor.settingModified" : {
572 573 574 575 576 577 578 579 580 581
				"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" }
			}
		*/
582
		this.telemetryService.publicLog('settingsEditor.settingModified', data);
R
Rob Lourens 已提交
583 584
	}

585
	private render(token: CancellationToken): TPromise<any> {
R
Rob Lourens 已提交
586 587
		if (this.input) {
			return this.input.resolve()
588 589 590 591 592 593
				.then((model: DefaultSettingsEditorModel) => {
					if (token.isCancellationRequested) {
						return void 0;
					}

					this.defaultSettingsEditorModel = model;
594
					this.onConfigUpdate();
595
				});
R
Rob Lourens 已提交
596 597 598 599
		}
		return TPromise.as(null);
	}

600
	private toggleSearchMode(): void {
601 602 603 604
		DOM.removeClass(this.rootElement, 'search-mode');
		if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') {
			DOM.toggleClass(this.rootElement, 'search-mode', !!this.searchResultModel);
		}
605 606
	}

607
	private onConfigUpdate(): TPromise<void> {
608
		const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
609 610 611
		const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core');
		const resolvedSettingsRoot = resolveSettingsTree(tocData, dividedGroups.core);
		const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core);
612 613
		resolvedSettingsRoot.children.unshift(commonlyUsed);

614 615
		resolvedSettingsRoot.children.push(resolveExtensionsSettings(dividedGroups.extension || []));

616 617 618 619
		if (this.searchResultModel) {
			this.searchResultModel.updateChildren();
		}

620 621 622 623
		if (this.settingsTreeModel) {
			this.settingsTreeModel.update(resolvedSettingsRoot);
		} else {
			this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, resolvedSettingsRoot);
624
			this.settingsTree.setInput(this.settingsTreeModel.root);
625 626 627 628 629 630 631

			this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement;
			if (this.tocTree.getInput()) {
				this.tocTree.refresh();
			} else {
				this.tocTree.setInput(this.tocTreeModel);
			}
632 633 634 635 636
		}

		return this.refreshTreeAndMaintainFocus();
	}

R
Rob Lourens 已提交
637 638 639 640 641 642 643 644
	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;

645 646 647
		return this.settingsTree.refresh()
			.then(() => {
				if (focusedRowId) {
648
					this.focusEditControlForRow(focusedRowId, selection);
R
Rob Lourens 已提交
649
				}
650 651 652 653
			})
			.then(() => {
				return this.tocTree.refresh();
			});
654 655
	}

656 657 658 659 660 661 662 663 664 665 666
	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);
			}
		}
	}

667
	private onSearchInputChanged(): void {
R
Rob Lourens 已提交
668 669
		const query = this.searchWidget.getValue().trim();
		this.delayedFilterLogging.cancel();
670
		this.triggerSearch(query).then(() => {
671
			if (query && this.searchResultModel) {
672
				this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel.getUniqueResults()));
673 674
			}
		});
R
Rob Lourens 已提交
675 676 677 678
	}

	private triggerSearch(query: string): TPromise<void> {
		if (query) {
R
Rob Lourens 已提交
679
			return this.searchInProgress = TPromise.join([
R
Rob Lourens 已提交
680
				this.localSearchDelayer.trigger(() => this.localFilterPreferences(query)),
681
				this.remoteSearchThrottle.trigger(() => this.remoteSearchPreferences(query), 500)
R
Rob Lourens 已提交
682 683 684
			]).then(() => {
				this.searchInProgress = null;
			});
R
Rob Lourens 已提交
685 686 687
		} else {
			this.localSearchDelayer.cancel();
			this.remoteSearchThrottle.cancel();
R
Rob Lourens 已提交
688 689 690
			if (this.searchInProgress && this.searchInProgress.cancel) {
				this.searchInProgress.cancel();
			}
R
Rob Lourens 已提交
691

692
			this.searchResultModel = null;
693
			this.tocTreeModel.currentSearchModel = null;
694
			this.viewState.filterToCategory = null;
695
			this.tocTree.refresh();
696
			this.toggleSearchMode();
697
			this.settingsTree.setInput(this.settingsTreeModel.root);
698

R
Rob Lourens 已提交
699 700 701 702
			return TPromise.wrap(null);
		}
	}

R
Rob Lourens 已提交
703 704 705 706 707 708
	private expandAll(tree: ITree): void {
		const nav = tree.getNavigator();
		let cur;
		while (cur = nav.next()) {
			tree.expand(cur);
		}
709 710
	}

711 712 713 714 715 716 717 718 719 720
	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];
721 722 723 724
		if (filterResult) {
			counts['filterResult'] = filterResult.filterMatches.length;
		}

725 726
		if (nlpResult) {
			counts['nlpResult'] = nlpResult.filterMatches.length;
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
		}

		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 }
			}
		*/
747
		this.telemetryService.publicLog('settingsEditor.filter', data);
748 749
	}

R
Rob Lourens 已提交
750
	private localFilterPreferences(query: string): TPromise<void> {
751 752
		const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider);
R
Rob Lourens 已提交
753 754 755
	}

	private remoteSearchPreferences(query: string): TPromise<void> {
756 757
		const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider);
R
Rob Lourens 已提交
758 759 760 761 762
	}

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

R
Rob Lourens 已提交
763 764 765 766 767 768 769
		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;
				}
770

R
Rob Lourens 已提交
771 772
				const [result] = results;
				if (!this.searchResultModel) {
773
					this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState);
774
					this.searchResultModel.setResult(type, result);
775
					this.tocTreeModel.currentSearchModel = this.searchResultModel;
776
					this.toggleSearchMode();
R
Rob Lourens 已提交
777
					this.settingsTree.setInput(this.searchResultModel);
778 779
				} else {
					this.searchResultModel.setResult(type, result);
R
Rob Lourens 已提交
780 781
				}

782
				this.tocTreeModel.update();
R
Rob Lourens 已提交
783 784 785 786
				resolve(this.refreshTreeAndMaintainFocus());
			});
		}, () => {
			isCanceled = true;
R
Rob Lourens 已提交
787 788 789 790 791 792 793 794 795 796 797
		});
	}

	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__
798
						"settingsEditor.searchError" : {
R
Rob Lourens 已提交
799 800 801 802 803 804 805
							"message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" },
							"filter": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
						}
					*/
					const message = getErrorMessage(err).trim();
					if (message && message !== 'Error') {
						// "Error" = any generic network error
806
						this.telemetryService.publicLog('settingsEditor.searchError', { message, filter });
R
Rob Lourens 已提交
807 808 809 810 811 812 813
						this.logService.info('Setting search error: ' + message);
					}
					return null;
				}
			});
	}

814
	private layoutTrees(dimension: DOM.Dimension): void {
815
		const listHeight = dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 12 /*padding*/);
816 817
		this.settingsTreeContainer.style.height = `${listHeight}px`;
		this.settingsTree.layout(listHeight, 800);
818

819 820 821 822 823
		const selectedSetting = this.settingsTree.getSelection()[0];
		if (selectedSetting) {
			this.settingsTree.refresh(selectedSetting);
		}

824 825 826
		const tocHeight = listHeight - 5; // padding
		this.tocTreeContainer.style.height = `${tocHeight}px`;
		this.tocTree.layout(tocHeight, 175);
827
	}
R
Rob Lourens 已提交
828
}