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

40 41
const $ = DOM.$;

R
Rob Lourens 已提交
42 43 44 45 46 47
export class SettingsEditor2 extends BaseEditor {

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

	private defaultSettingsEditorModel: DefaultSettingsEditorModel;

48
	private rootElement: HTMLElement;
R
Rob Lourens 已提交
49 50 51 52
	private headerContainer: HTMLElement;
	private searchWidget: SearchWidget;
	private settingsTargetsWidget: SettingsTargetsWidget;

53
	private showConfiguredSettingsOnlyCheckbox: HTMLInputElement;
R
Rob Lourens 已提交
54

55 56 57
	private settingsTreeContainer: HTMLElement;
	private settingsTree: WorkbenchTree;
	private treeDataSource: SettingsDataSource;
58
	private tocTreeModel: TOCTreeModel;
59
	private settingsTreeModel: SettingsTreeModel;
R
Rob Lourens 已提交
60

R
Rob Lourens 已提交
61 62 63
	private tocTreeContainer: HTMLElement;
	private tocTree: WorkbenchTree;

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

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

72
	private selectedElement: SettingsTreeElement;
73

74
	private viewState: ISettingsEditorViewState;
75
	private searchResultModel: SearchResultModel;
76 77
	private inSettingsEditorContextKey: IContextKey<boolean>;
	private searchFocusContextKey: IContextKey<boolean>;
78

R
Rob Lourens 已提交
79 80 81 82 83
	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
		@IConfigurationService private configurationService: IConfigurationService,
		@IThemeService themeService: IThemeService,
		@IPreferencesService private preferencesService: IPreferencesService,
R
Rob Lourens 已提交
84 85
		@IInstantiationService private instantiationService: IInstantiationService,
		@IPreferencesSearchService private preferencesSearchService: IPreferencesSearchService,
86
		@ILogService private logService: ILogService,
87 88
		@IEnvironmentService private environmentService: IEnvironmentService,
		@IContextKeyService contextKeyService: IContextKeyService
R
Rob Lourens 已提交
89 90
	) {
		super(SettingsEditor2.ID, telemetryService, themeService);
R
Rob Lourens 已提交
91 92 93
		this.delayedFilterLogging = new Delayer<void>(1000);
		this.localSearchDelayer = new Delayer(100);
		this.remoteSearchThrottle = new ThrottledDelayer(200);
94
		this.viewState = { settingsTarget: ConfigurationTarget.USER };
R
Rob Lourens 已提交
95

96 97
		this.settingUpdateDelayer = new Delayer<void>(500);

98 99 100
		this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);
		this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);

101
		this._register(configurationService.onDidChangeConfiguration(() => this.onConfigUpdate()));
R
Rob Lourens 已提交
102 103 104
	}

	createEditor(parent: HTMLElement): void {
105
		this.rootElement = DOM.append(parent, $('.settings-editor'));
R
Rob Lourens 已提交
106

107 108
		this.createHeader(this.rootElement);
		this.createBody(this.rootElement);
R
Rob Lourens 已提交
109 110
	}

111
	setInput(input: SettingsEditor2Input, options: EditorOptions, token: CancellationToken): Thenable<void> {
112
		this.inSettingsEditorContextKey.set(true);
113
		return super.setInput(input, options, token)
R
Rob Lourens 已提交
114
			.then(() => {
115
				this.render(token);
R
Rob Lourens 已提交
116 117 118
			});
	}

119 120 121 122 123
	clearInput(): void {
		this.inSettingsEditorContextKey.set(false);
		super.clearInput();
	}

R
Rob Lourens 已提交
124 125
	layout(dimension: DOM.Dimension): void {
		this.searchWidget.layout(dimension);
126
		this.layoutSettingsList(dimension);
R
Rob Lourens 已提交
127 128 129
	}

	focus(): void {
130 131 132 133
		this.focusSearch();
	}

	focusSearch(): void {
R
Rob Lourens 已提交
134 135 136
		this.searchWidget.focus();
	}

137 138 139 140
	clearSearchResults(): void {
		this.searchWidget.clear();
	}

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

144
		const previewHeader = DOM.append(this.headerContainer, $('.settings-preview-header'));
R
Rob Lourens 已提交
145 146 147 148 149

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

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

R
Rob Lourens 已提交
152 153 154
		const searchContainer = DOM.append(this.headerContainer, $('.search-container'));
		this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, {
			ariaLabel: localize('SearchSettings.AriaLabel', "Search settings"),
155 156
			placeholder: localize('SearchSettings.Placeholder', "Search settings"),
			focusKey: this.searchFocusContextKey
R
Rob Lourens 已提交
157
		}));
158
		this._register(this.searchWidget.onDidChange(() => this.onSearchInputChanged()));
159 160
		this._register(DOM.addStandardDisposableListener(this.searchWidget.domNode, 'keydown', e => {
			if (e.keyCode === KeyCode.DownArrow) {
161 162
				this.settingsTree.focusFirst();
				this.settingsTree.domFocus();
163 164
			}
		}));
R
Rob Lourens 已提交
165

166 167 168 169 170 171 172 173 174 175 176 177 178 179
		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()));

180
		const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
R
Rob Lourens 已提交
181 182 183
		const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
		this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer));
		this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER;
184 185 186 187
		this.settingsTargetsWidget.onDidTargetChange(() => {
			this.viewState.settingsTarget = this.settingsTargetsWidget.settingsTarget;
			this.settingsTree.refresh();
		});
R
Rob Lourens 已提交
188

189
		this.createHeaderControls(headerControlsContainer);
R
Rob Lourens 已提交
190 191
	}

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

195 196 197
		this.showConfiguredSettingsOnlyCheckbox = DOM.append(headerControlsContainerRight, $('input#configured-only-checkbox'));
		this.showConfiguredSettingsOnlyCheckbox.type = 'checkbox';
		const showConfiguredSettingsOnlyLabel = <HTMLLabelElement>DOM.append(headerControlsContainerRight, $('label.configured-only-label'));
198
		showConfiguredSettingsOnlyLabel.textContent = localize('showOverriddenOnly', "Show modified only");
199
		showConfiguredSettingsOnlyLabel.htmlFor = 'configured-only-checkbox';
200

201
		this._register(DOM.addDisposableListener(this.showConfiguredSettingsOnlyCheckbox, 'change', e => this.onShowConfiguredOnlyClicked()));
202 203 204 205 206 207 208 209 210 211 212 213
	}

	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 已提交
214 215 216 217 218
	}

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

219
		this.createTOC(bodyContainer);
R
Rob Lourens 已提交
220
		this.createSettingsTree(bodyContainer);
221 222 223 224

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

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

230 231 232
		const tocDataSource = this.instantiationService.createInstance(TOCDataSource);
		const tocRenderer = this.instantiationService.createInstance(TOCRenderer);
		this.tocTreeModel = new TOCTreeModel();
R
Rob Lourens 已提交
233 234 235

		this.tocTree = this.instantiationService.createInstance(WorkbenchTree, this.tocTreeContainer,
			<ITreeConfiguration>{
236 237
				dataSource: tocDataSource,
				renderer: tocRenderer,
238
				filter: this.instantiationService.createInstance(SettingsTreeFilter, this.viewState)
R
Rob Lourens 已提交
239 240
			},
			{
241 242
				showLoading: false,
				twistiePixels: 15
R
Rob Lourens 已提交
243 244 245
			});

		this._register(this.tocTree.onDidChangeSelection(e => {
246
			if (this.settingsTreeModel) {
247 248 249 250 251 252 253 254 255 256 257 258
				const element = e.selection[0];
				const currentSelection = this.settingsTree.getSelection()[0];
				const isEqualOrParent = (element: SettingsTreeElement, candidate: SettingsTreeElement) => {
					do {
						if (element === candidate) {
							return true;
						}
					} while (element = element.parent);

					return false;
				};

R
Rob Lourens 已提交
259
				if (element && (!currentSelection || !isEqualOrParent(currentSelection, element))) {
260 261 262 263
					this.settingsTree.reveal(element, 0);
					this.settingsTree.setSelection([element]);
					this.settingsTree.setFocus(element);
				}
264
			}
R
Rob Lourens 已提交
265 266 267 268
		}));
	}

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

271
		this.treeDataSource = this.instantiationService.createInstance(SettingsDataSource, this.viewState);
272
		const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer);
273
		this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value)));
274
		this._register(renderer.onDidOpenSettings(() => this.openSettingsFile()));
275

276
		const treeClass = 'settings-editor-tree';
277
		this.settingsTree = this.instantiationService.createInstance(NonExpandableTree, this.settingsTreeContainer,
278
			<ITreeConfiguration>{
279
				dataSource: this.treeDataSource,
R
Rob Lourens 已提交
280
				renderer,
281 282
				controller: this.instantiationService.createInstance(SettingsTreeController),
				accessibilityProvider: this.instantiationService.createInstance(SettingsAccessibilityProvider),
283 284
				filter: this.instantiationService.createInstance(SettingsTreeFilter, this.viewState),
				styler: new DefaultTreestyler(DOM.createStyleSheet(), treeClass)
285 286 287 288
			},
			{
				ariaLabel: localize('treeAriaLabel', "Settings"),
				showLoading: false,
289 290
				indentPixels: 0,
				twistiePixels: 0,
291
			});
292

293
		this._register(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
294 295
			const activeBorderColor = theme.getColor(listActiveSelectionBackground);
			if (activeBorderColor) {
296
				collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree:focus .monaco-tree-row.focused {outline: solid 1px ${activeBorderColor}; outline-offset: -1px; }`);
297 298
			}

299 300
			const inactiveBorderColor = theme.getColor(listInactiveSelectionBackground);
			if (inactiveBorderColor) {
301
				collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree .monaco-tree-row.focused {outline: solid 1px ${inactiveBorderColor}; outline-offset: -1px; }`);
302
			}
303 304
		}));

305 306
		this.settingsTree.getHTMLElement().classList.add(treeClass);

307
		this._register(attachStyler(this.themeService, {
308 309 310 311 312 313 314 315 316 317
			listActiveSelectionBackground: editorBackground,
			listActiveSelectionForeground: foreground,
			listFocusAndSelectionBackground: editorBackground,
			listFocusAndSelectionForeground: foreground,
			listFocusBackground: editorBackground,
			listFocusForeground: foreground,
			listHoverForeground: foreground,
			listHoverBackground: editorBackground,
			listInactiveSelectionBackground: editorBackground,
			listInactiveSelectionForeground: foreground
318 319
		}, colors => {
			this.settingsTree.style(colors);
320
		}));
321

322
		this._register(this.settingsTree.onDidChangeFocus(e => {
323
			this.settingsTree.setSelection([e.focus]);
324 325
			if (this.selectedElement) {
				this.settingsTree.refresh(this.selectedElement);
326
			}
327

328 329
			if (e.focus) {
				this.settingsTree.refresh(e.focus);
330
			}
331 332

			this.selectedElement = e.focus;
333 334 335 336 337 338 339 340 341 342 343 344 345
		}));

		this._register(this.settingsTree.onDidChangeSelection(e => {
			const element = e.selection[0] instanceof SettingsTreeSettingElement ? e.selection[0].parent :
				e.selection[0] instanceof SettingsTreeGroupElement ? e.selection[0] :
					null;

			if (element && this.tocTree.getSelection()[0] !== element) {
				this.tocTree.reveal(element, 0);
				this.tocTree.setSelection([element]);
				this.tocTree.setFocus(element);
			}
		}));
346 347
	}

348 349 350 351 352 353 354 355 356 357 358 359
	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');
		}));
	}

360
	private onShowConfiguredOnlyClicked(): void {
361
		this.viewState.showConfiguredOnly = this.showConfiguredSettingsOnlyCheckbox.checked;
R
Rob Lourens 已提交
362
		this.refreshTreeAndMaintainFocus();
363 364
		this.tocTree.refresh();
		this.settingsTree.setScrollPosition(0);
365
		this.expandAll(this.settingsTree);
366 367
	}

R
Rob Lourens 已提交
368
	private onDidChangeSetting(key: string, value: any): void {
369 370
		if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) {
			this.updateChangedSetting(key, value);
371 372
		}

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
		this.pendingSettingUpdate = { key, value };
		this.settingUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
	}

	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
		return this.configurationService.updateValue(key, value, <ConfigurationTarget>this.settingsTargetsWidget.settingsTarget)
			.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);
			});
395 396 397
	}

	private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[], rawResults: ISearchResult[], showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void {
398
		this.pendingSettingUpdate = null;
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415

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

416 417 418 419 420 421
			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;
				}
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
			}
		}

		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__
441
			"settingsEditor.settingModified" : {
442 443 444 445 446 447 448 449 450 451
				"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" }
			}
		*/
452
		this.telemetryService.publicLog('settingsEditor.settingModified', data);
R
Rob Lourens 已提交
453 454
	}

455
	private render(token: CancellationToken): TPromise<any> {
R
Rob Lourens 已提交
456 457
		if (this.input) {
			return this.input.resolve()
458 459 460 461 462 463
				.then((model: DefaultSettingsEditorModel) => {
					if (token.isCancellationRequested) {
						return void 0;
					}

					this.defaultSettingsEditorModel = model;
464
					this.onConfigUpdate();
465
				});
R
Rob Lourens 已提交
466 467 468 469
		}
		return TPromise.as(null);
	}

470
	private toggleSearchMode(): void {
471 472 473 474
		DOM.removeClass(this.rootElement, 'search-mode');
		if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') {
			DOM.toggleClass(this.rootElement, 'search-mode', !!this.searchResultModel);
		}
475 476
	}

477
	private onConfigUpdate(): TPromise<void> {
478 479 480 481 482
		const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
		const resolvedSettingsRoot = resolveSettingsTree(tocData, groups);
		const commonlyUsed = resolveSettingsTree(commonlyUsedData, groups);
		resolvedSettingsRoot.children.unshift(commonlyUsed);

483 484 485 486
		if (this.settingsTreeModel) {
			this.settingsTreeModel.update(resolvedSettingsRoot);
		} else {
			this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, resolvedSettingsRoot);
487
			this.settingsTree.setInput(this.settingsTreeModel.root);
488 489 490 491 492 493 494

			this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement;
			if (this.tocTree.getInput()) {
				this.tocTree.refresh();
			} else {
				this.tocTree.setInput(this.tocTreeModel);
			}
495 496 497 498 499
		}

		return this.refreshTreeAndMaintainFocus();
	}

R
Rob Lourens 已提交
500 501 502 503 504 505 506 507
	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;

508 509 510 511 512 513 514 515 516 517
		return this.settingsTree.refresh()
			.then(() => {
				if (focusedRowId) {
					const rowSelector = `.setting-item#${focusedRowId}`;
					const inputElementToFocus: HTMLElement = this.settingsTreeContainer.querySelector(`${rowSelector} input, ${rowSelector} select, ${rowSelector} a`);
					if (inputElementToFocus) {
						inputElementToFocus.focus();
						if (typeof selection === 'number') {
							(<HTMLInputElement>inputElementToFocus).setSelectionRange(selection, selection);
						}
R
Rob Lourens 已提交
518 519
					}
				}
520 521 522 523
			})
			.then(() => {
				return this.tocTree.refresh();
			});
524 525
	}

526
	private onSearchInputChanged(): void {
R
Rob Lourens 已提交
527 528
		const query = this.searchWidget.getValue().trim();
		this.delayedFilterLogging.cancel();
529
		this.triggerSearch(query).then(() => {
530
			if (query && this.searchResultModel) {
531
				this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel.getUniqueResults()));
532 533
			}
		});
R
Rob Lourens 已提交
534 535 536 537
	}

	private triggerSearch(query: string): TPromise<void> {
		if (query) {
R
Rob Lourens 已提交
538
			return this.searchInProgress = TPromise.join([
R
Rob Lourens 已提交
539
				this.localSearchDelayer.trigger(() => this.localFilterPreferences(query)),
540
				this.remoteSearchThrottle.trigger(() => this.remoteSearchPreferences(query), 500)
R
Rob Lourens 已提交
541 542 543
			]).then(() => {
				this.searchInProgress = null;
			});
R
Rob Lourens 已提交
544 545 546
		} else {
			this.localSearchDelayer.cancel();
			this.remoteSearchThrottle.cancel();
R
Rob Lourens 已提交
547 548 549
			if (this.searchInProgress && this.searchInProgress.cancel) {
				this.searchInProgress.cancel();
			}
R
Rob Lourens 已提交
550

551
			this.searchResultModel = null;
552 553
			this.tocTreeModel.currentSearchModel = null;
			this.tocTree.refresh();
554
			this.toggleSearchMode();
555
			this.settingsTree.setInput(this.settingsTreeModel.root);
556

R
Rob Lourens 已提交
557 558 559 560
			return TPromise.wrap(null);
		}
	}

R
Rob Lourens 已提交
561 562 563 564 565 566
	private expandAll(tree: ITree): void {
		const nav = tree.getNavigator();
		let cur;
		while (cur = nav.next()) {
			tree.expand(cur);
		}
567 568
	}

569 570 571 572 573 574 575 576 577 578 579
	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];
		counts['filterResult'] = filterResult.filterMatches.length;
580 581
		if (nlpResult) {
			counts['nlpResult'] = nlpResult.filterMatches.length;
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
		}

		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 }
			}
		*/
602
		this.telemetryService.publicLog('settingsEditor.filter', data);
603 604
	}

R
Rob Lourens 已提交
605
	private localFilterPreferences(query: string): TPromise<void> {
606 607
		const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider);
R
Rob Lourens 已提交
608 609 610
	}

	private remoteSearchPreferences(query: string): TPromise<void> {
611 612
		const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider);
R
Rob Lourens 已提交
613 614 615 616 617
	}

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

R
Rob Lourens 已提交
618 619 620 621 622 623 624
		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;
				}
625

R
Rob Lourens 已提交
626 627 628
				const [result] = results;
				if (!this.searchResultModel) {
					this.searchResultModel = new SearchResultModel();
629
					this.tocTreeModel.currentSearchModel = this.searchResultModel;
630
					this.toggleSearchMode();
R
Rob Lourens 已提交
631 632 633 634
					this.settingsTree.setInput(this.searchResultModel);
				}

				this.searchResultModel.setResult(type, result);
635
				this.tocTreeModel.update();
R
Rob Lourens 已提交
636 637 638 639
				resolve(this.refreshTreeAndMaintainFocus());
			});
		}, () => {
			isCanceled = true;
R
Rob Lourens 已提交
640 641 642 643 644 645 646 647 648 649 650
		});
	}

	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__
651
						"settingsEditor.searchError" : {
R
Rob Lourens 已提交
652 653 654 655 656 657 658
							"message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" },
							"filter": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
						}
					*/
					const message = getErrorMessage(err).trim();
					if (message && message !== 'Error') {
						// "Error" = any generic network error
659
						this.telemetryService.publicLog('settingsEditor.searchError', { message, filter });
R
Rob Lourens 已提交
660 661 662 663 664 665 666
						this.logService.info('Setting search error: ' + message);
					}
					return null;
				}
			});
	}

667 668
	private layoutSettingsList(dimension: DOM.Dimension): void {
		const listHeight = dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 12 /*padding*/);
669
		this.settingsTreeContainer.style.height = `${listHeight}px`;
R
Rob Lourens 已提交
670
		this.tocTreeContainer.style.height = `${listHeight}px`;
671
		this.settingsTree.layout(listHeight, 800);
R
Rob Lourens 已提交
672
		this.tocTree.layout(listHeight, 200);
673
	}
R
Rob Lourens 已提交
674
}