settingsEditor2.ts 31.3 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 { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
8
import { Button } from 'vs/base/browser/ui/button/button';
9 10
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { Action } from 'vs/base/common/actions';
11
import * as arrays from 'vs/base/common/arrays';
R
Rob Lourens 已提交
12
import { Delayer, ThrottledDelayer } from 'vs/base/common/async';
R
Rob Lourens 已提交
13
import { CancellationToken } from 'vs/base/common/cancellation';
R
Rob Lourens 已提交
14
import * as collections from 'vs/base/common/collections';
15
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
R
Rob Lourens 已提交
16
import URI from 'vs/base/common/uri';
R
Rob Lourens 已提交
17
import { TPromise } from 'vs/base/common/winjs.base';
18
import { collapseAll, expandAll } from 'vs/base/parts/tree/browser/treeUtils';
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 } 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 } 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 { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor';
34
import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets';
R
Rob Lourens 已提交
35
import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout';
36 37
import { ISettingsEditorViewState, MODIFIED_SETTING_TAG, ONLINE_SERVICES_SETTING_TAG, resolveExtensionsSettings, resolveSettingsTree, SearchResultIdx, SearchResultModel, SettingsRenderer, SettingsTree, SettingsTreeElement, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTree';
import { TOCRenderer, TOCTree, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree';
38
import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences';
39
import { IPreferencesService, ISearchResult, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences';
40
import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
R
Rob Lourens 已提交
41
import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
R
Rob Lourens 已提交
42

43 44
const $ = DOM.$;

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

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

	private defaultSettingsEditorModel: DefaultSettingsEditorModel;

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

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

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

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

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

73
	private viewState: ISettingsEditorViewState;
74
	private searchResultModel: SearchResultModel;
75

76
	private tocRowFocused: IContextKey<boolean>;
77 78
	private inSettingsEditorContextKey: IContextKey<boolean>;
	private searchFocusContextKey: IContextKey<boolean>;
79

80 81
	private tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g;

82 83 84
	/** Don't spam warnings */
	private hasWarnedMissingSettings: boolean;

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

103 104
		this.settingUpdateDelayer = new Delayer<void>(500);

105 106
		this.inSettingsEditorContextKey = CONTEXT_SETTINGS_EDITOR.bindTo(contextKeyService);
		this.searchFocusContextKey = CONTEXT_SETTINGS_SEARCH_FOCUS.bindTo(contextKeyService);
107
		this.tocRowFocused = CONTEXT_TOC_ROW_FOCUS.bindTo(contextKeyService);
108

109 110 111 112 113 114 115
		this._register(configurationService.onDidChangeConfiguration(e => {
			this.onConfigUpdate();

			if (e.affectsConfiguration('workbench.settings.tocVisible')) {
				this.updateTOCVisible();
			}
		}));
R
Rob Lourens 已提交
116 117 118
	}

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

122 123
		this.createHeader(this.rootElement);
		this.createBody(this.rootElement);
R
Rob Lourens 已提交
124 125
	}

126
	setInput(input: SettingsEditor2Input, options: EditorOptions, token: CancellationToken): Thenable<void> {
127
		this.inSettingsEditorContextKey.set(true);
128
		return super.setInput(input, options, token)
R
Rob Lourens 已提交
129
			.then(() => {
130 131
				return this.render(token);
			}).then(() => new Promise(process.nextTick)); // Force setInput to be async
R
Rob Lourens 已提交
132 133
	}

134 135 136 137 138
	clearInput(): void {
		this.inSettingsEditorContextKey.set(false);
		super.clearInput();
	}

R
Rob Lourens 已提交
139 140
	layout(dimension: DOM.Dimension): void {
		this.searchWidget.layout(dimension);
141 142 143
		this.layoutTrees(dimension);

		DOM.toggleClass(this.rootElement, 'narrow', dimension.width < 600);
R
Rob Lourens 已提交
144 145 146
	}

	focus(): void {
147 148 149
		this.focusSearch();
	}

150
	focusSettings(): void {
151 152 153
		const firstFocusable = this.settingsTree.getHTMLElement().querySelector('a, input, [tabindex="0"]');
		if (firstFocusable) {
			(<HTMLElement>firstFocusable).focus();
154 155 156
		}
	}

157
	focusSearch(): void {
R
Rob Lourens 已提交
158 159 160
		this.searchWidget.focus();
	}

161 162 163 164 165 166 167 168
	editSelectedSetting(): void {
		const focus = this.settingsTree.getFocus();
		if (focus instanceof SettingsTreeSettingElement) {
			const itemId = focus.id.replace(/\./g, '_');
			this.focusEditControlForRow(itemId);
		}
	}

169 170 171 172
	clearSearchResults(): void {
		this.searchWidget.clear();
	}

173
	search(text: string): void {
174
		if (this.searchWidget) {
175
			this.searchWidget.focus();
176
			this.searchWidget.setValue(text);
177 178 179
		}
	}

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

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

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

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

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

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

			this.settingsTreeModel.update();
			this.refreshTreeAndMaintainFocus();
210
		});
R
Rob Lourens 已提交
211

212
		this.createHeaderControls(headerControlsContainer);
R
Rob Lourens 已提交
213 214
	}

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

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

223
		const actions: Action[] = [
224
			this.instantiationService.createInstance(FilterByTagAction,
225
				localize('filterModifiedLabel', "Show modified settings"),
226
				MODIFIED_SETTING_TAG,
227
				this)
228
		];
229 230 231 232 233 234 235 236 237 238 239
		if (this.environmentService.appQuality !== 'stable') {
			actions.push(
				this.instantiationService.createInstance(
					FilterByTagAction,
					localize('filterOnlineServicesLabel', "Show settings for online services"),
					ONLINE_SERVICES_SETTING_TAG,
					this));
			actions.push(new Separator());
		}
		actions.push(this.instantiationService.createInstance(OpenSettingsAction));

240
		this.toolbar.setActions([], actions)();
241
		this.toolbar.context = <ISettingsToolbarContext>{ target: this.settingsTargetsWidget.settingsTarget };
242 243
	}

244 245 246
	private revealSetting(settingName: string): void {
		const element = this.settingsTreeModel.getElementByName(settingName);
		if (element) {
247
			this.settingsTree.reveal(element, 0);
248 249 250
		}
	}

251 252 253 254 255 256 257 258 259
	private revealSettingElement(element: SettingsTreeElement): void {
		const top = this.settingsTree.getRelativeTop(element);
		const clampedTop = Math.max(
			Math.min(top, .9),
			.1);

		this.settingsTree.reveal(element, clampedTop);
	}

260 261 262 263 264 265 266 267 268 269
	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 已提交
270 271 272 273 274
	}

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

275 276 277 278
		this.createFocusSink(bodyContainer, () => {
			const firstElement = this.settingsTree.getFirstVisibleElement();
			this.settingsTree.reveal(firstElement, 0.1);
		});
R
Rob Lourens 已提交
279
		this.createSettingsTree(bodyContainer);
280 281 282 283 284
		this.createFocusSink(bodyContainer, () => {
			const lastElement = this.settingsTree.getLastVisibleElement();
			this.settingsTree.reveal(lastElement, 0.9);
		});

285
		this.createTOC(bodyContainer);
286 287 288 289

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

292 293 294 295 296 297 298 299 300 301 302 303 304
	private createFocusSink(container: HTMLElement, callback: () => void): HTMLElement {
		const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink'));
		listFocusSink.tabIndex = 0;
		this._register(DOM.addDisposableListener(listFocusSink, 'focus', e => {
			if (e.relatedTarget && DOM.findParentWithClass(e.relatedTarget, 'monaco-tree')) {
				callback();
				e.relatedTarget.focus();
			}
		}));

		return listFocusSink;
	}

R
Rob Lourens 已提交
305
	private createTOC(parent: HTMLElement): void {
306
		this.tocTreeModel = new TOCTreeModel();
R
Rob Lourens 已提交
307 308
		this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container'));

309
		const tocRenderer = this.instantiationService.createInstance(TOCRenderer);
R
Rob Lourens 已提交
310

311
		this.tocTree = this._register(this.instantiationService.createInstance(TOCTree, this.tocTreeContainer,
312
			this.viewState,
R
Rob Lourens 已提交
313
			{
314
				renderer: tocRenderer
315
			}));
R
Rob Lourens 已提交
316

317
		this._register(this.tocTree.onDidChangeFocus(e => {
R
Rob Lourens 已提交
318 319 320 321 322 323 324
			// Let the caller finish before trying to sync with settings tree.
			// e.g. clicking this twistie, which will toggle the row's expansion state _after_ this event is fired.
			process.nextTick(() => {
				const element = e.focus;
				if (this.searchResultModel) {
					this.viewState.filterToCategory = element;
					this.refreshTreeAndMaintainFocus();
325
				}
R
Rob Lourens 已提交
326
			});
327 328 329 330 331 332 333 334
		}));

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

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

		this.updateTOCVisible();
	}

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

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

348
		const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer);
349
		this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value)));
350 351 352 353 354 355 356
		this._register(renderer.onDidOpenSettings(settingKey => {
			this.openSettingsFile().then(editor => {
				if (editor instanceof PreferencesEditor && settingKey) {
					editor.focusSearch(settingKey);
				}
			});
		}));
357
		this._register(renderer.onDidClickSettingLink(settingName => this.revealSetting(settingName)));
358
		this._register(renderer.onDidFocusSetting(element => this.revealSettingElement(element)));
359

360
		this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree,
361 362
			this.settingsTreeContainer,
			this.viewState,
363
			{
364
				renderer
365
			}));
366
		this.settingsTree.getHTMLElement().tabIndex = -1;
367

368 369
		this._register(this.settingsTree.onDidScroll(() => {
			this.updateTreeScrollSync();
370
		}));
371 372
	}

373 374 375 376 377 378 379 380 381 382 383 384
	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');
		}));
	}

R
Rob Lourens 已提交
385
	private onDidChangeSetting(key: string, value: any): void {
386 387
		if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) {
			this.updateChangedSetting(key, value);
388 389
		}

390 391 392 393
		this.pendingSettingUpdate = { key, value };
		this.settingUpdateDelayer.trigger(() => this.updateChangedSetting(key, value));
	}

394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
	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) {
417
			this.tocTree.reveal(element);
418
			const elementTop = this.tocTree.getRelativeTop(element);
419 420 421
			collapseAll(this.tocTree, element);
			if (elementTop < 0 || elementTop > 1) {
				this.tocTree.reveal(element);
422 423
			} else {
				this.tocTree.reveal(element, elementTop);
424 425
			}

426 427
			this.tocTree.setSelection([element]);
			this.tocTree.setFocus(element, { fromScroll: true });
428 429 430
		}
	}

431 432 433
	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 已提交
434 435 436 437 438 439 440 441 442 443 444 445
		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)
446 447 448 449 450 451 452
			.then(() => this.refreshTreeAndMaintainFocus())
			.then(() => {
				const reportModifiedProps = {
					key,
					query: this.searchWidget.getValue(),
					searchResults: this.searchResultModel && this.searchResultModel.getUniqueResults(),
					rawResults: this.searchResultModel && this.searchResultModel.getRawResults(),
453
					showConfiguredOnly: this.viewState.tagFilters && this.viewState.tagFilters.has(MODIFIED_SETTING_TAG),
454 455 456 457 458 459
					isReset: typeof value === 'undefined',
					settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget
				};

				return this.reportModifiedSetting(reportModifiedProps);
			});
460 461 462
	}

	private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[], rawResults: ISearchResult[], showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void {
463
		this.pendingSettingUpdate = null;
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480

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

481 482 483 484 485 486
			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;
				}
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
			}
		}

		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__
506
			"settingsEditor.settingModified" : {
507 508 509 510 511 512 513 514 515 516
				"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" }
			}
		*/
517
		this.telemetryService.publicLog('settingsEditor.settingModified', data);
R
Rob Lourens 已提交
518 519
	}

520
	private render(token: CancellationToken): TPromise<any> {
R
Rob Lourens 已提交
521 522
		if (this.input) {
			return this.input.resolve()
523 524 525 526 527
				.then((model: DefaultSettingsEditorModel) => {
					if (token.isCancellationRequested) {
						return void 0;
					}

528
					this._register(model.onDidChangeGroups(() => this.onConfigUpdate()));
529
					this.defaultSettingsEditorModel = model;
530
					return this.onConfigUpdate();
531
				});
R
Rob Lourens 已提交
532 533 534 535
		}
		return TPromise.as(null);
	}

536
	private toggleSearchMode(): void {
537 538 539 540
		DOM.removeClass(this.rootElement, 'search-mode');
		if (this.configurationService.getValue('workbench.settings.settingsSearchTocBehavior') === 'hide') {
			DOM.toggleClass(this.rootElement, 'search-mode', !!this.searchResultModel);
		}
541 542
	}

543
	private onConfigUpdate(): TPromise<void> {
544
		const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
545
		const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core');
546 547 548 549 550 551 552 553 554 555 556 557 558 559
		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;
		}

560
		const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core);
561
		resolvedSettingsRoot.children.unshift(commonlyUsed.tree);
562

563 564
		resolvedSettingsRoot.children.push(resolveExtensionsSettings(dividedGroups.extension || []));

565 566 567 568
		if (this.searchResultModel) {
			this.searchResultModel.updateChildren();
		}

569 570 571 572
		if (this.settingsTreeModel) {
			this.settingsTreeModel.update(resolvedSettingsRoot);
		} else {
			this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, resolvedSettingsRoot);
573
			this.settingsTree.setInput(this.settingsTreeModel.root);
574 575 576 577 578 579 580

			this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement;
			if (this.tocTree.getInput()) {
				this.tocTree.refresh();
			} else {
				this.tocTree.setInput(this.tocTreeModel);
			}
581 582 583 584 585
		}

		return this.refreshTreeAndMaintainFocus();
	}

R
Rob Lourens 已提交
586 587 588 589 590 591 592 593
	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;

594 595 596
		return this.settingsTree.refresh()
			.then(() => {
				if (focusedRowId) {
597
					this.focusEditControlForRow(focusedRowId, selection);
R
Rob Lourens 已提交
598
				}
599 600
			})
			.then(() => {
601 602 603
				// TODO@roblou - hack
				this.tocTreeModel.update();

604 605
				return this.tocTree.refresh();
			});
606 607
	}

608 609
	private focusEditControlForRow(id: string, selection?: number): void {
		const rowSelector = `.setting-item#${id}`;
610
		const inputElementToFocus: HTMLElement = this.settingsTreeContainer.querySelector(`${rowSelector} input, ${rowSelector} select, ${rowSelector} .monaco-custom-checkbox`);
611 612 613 614 615 616 617 618
		if (inputElementToFocus) {
			inputElementToFocus.focus();
			if (typeof selection === 'number') {
				(<HTMLInputElement>inputElementToFocus).setSelectionRange(selection, selection);
			}
		}
	}

619
	private onSearchInputChanged(): void {
R
Rob Lourens 已提交
620 621
		const query = this.searchWidget.getValue().trim();
		this.delayedFilterLogging.cancel();
622
		this.triggerSearch(query).then(() => {
623
			if (query && this.searchResultModel) {
624
				this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel.getUniqueResults()));
625 626
			}
		});
R
Rob Lourens 已提交
627 628 629
	}

	private triggerSearch(query: string): TPromise<void> {
630 631
		this.viewState.tagFilters = new Set<string>();
		if (query) {
632 633 634 635 636 637 638 639
			query = query.replace(this.tagRegex, (_, __, quotedTag, tag) => {
				this.viewState.tagFilters.add(tag || quotedTag);
				return '';
			});
			query = query.replace(`@${MODIFIED_SETTING_TAG}`, () => {
				this.viewState.tagFilters.add(MODIFIED_SETTING_TAG);
				return '';
			});
640
		}
641
		query = query.trim();
R
Rob Lourens 已提交
642
		if (query) {
R
Rob Lourens 已提交
643
			return this.searchInProgress = TPromise.join([
R
Rob Lourens 已提交
644
				this.localSearchDelayer.trigger(() => this.localFilterPreferences(query)),
645
				this.remoteSearchThrottle.trigger(() => this.remoteSearchPreferences(query), 500)
R
Rob Lourens 已提交
646 647 648
			]).then(() => {
				this.searchInProgress = null;
			});
R
Rob Lourens 已提交
649 650 651
		} else {
			this.localSearchDelayer.cancel();
			this.remoteSearchThrottle.cancel();
R
Rob Lourens 已提交
652 653 654
			if (this.searchInProgress && this.searchInProgress.cancel) {
				this.searchInProgress.cancel();
			}
R
Rob Lourens 已提交
655

656
			this.searchResultModel = null;
657
			this.tocTreeModel.currentSearchModel = null;
658
			this.viewState.filterToCategory = null;
659
			this.tocTree.refresh();
660
			this.toggleSearchMode();
661 662
			collapseAll(this.tocTree);
			return this.settingsTree.setInput(this.settingsTreeModel.root);
R
Rob Lourens 已提交
663 664 665
		}
	}

666 667 668 669 670 671 672 673 674 675
	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];
676 677 678 679
		if (filterResult) {
			counts['filterResult'] = filterResult.filterMatches.length;
		}

680 681
		if (nlpResult) {
			counts['nlpResult'] = nlpResult.filterMatches.length;
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
		}

		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 }
			}
		*/
702
		this.telemetryService.publicLog('settingsEditor.filter', data);
703 704
	}

R
Rob Lourens 已提交
705
	private localFilterPreferences(query: string): TPromise<void> {
706 707
		const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider);
R
Rob Lourens 已提交
708 709 710
	}

	private remoteSearchPreferences(query: string): TPromise<void> {
711 712
		const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider);
R
Rob Lourens 已提交
713 714 715
	}

	private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider: ISearchProvider): TPromise<void> {
R
Rob Lourens 已提交
716 717
		let isCanceled = false;
		return new TPromise(resolve => {
718
			return this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider).then(result => {
R
Rob Lourens 已提交
719 720 721 722
				if (isCanceled) {
					// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
					return null;
				}
723

R
Rob Lourens 已提交
724
				if (!this.searchResultModel) {
725
					this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState);
726
					this.searchResultModel.setResult(type, result);
727
					this.tocTreeModel.currentSearchModel = this.searchResultModel;
728
					this.toggleSearchMode();
R
Rob Lourens 已提交
729
					this.settingsTree.setInput(this.searchResultModel);
730 731
				} else {
					this.searchResultModel.setResult(type, result);
R
Rob Lourens 已提交
732 733
				}

734
				this.tocTreeModel.update();
735 736
				expandAll(this.tocTree);

R
Rob Lourens 已提交
737 738 739 740
				resolve(this.refreshTreeAndMaintainFocus());
			});
		}, () => {
			isCanceled = true;
R
Rob Lourens 已提交
741 742 743 744 745 746 747 748 749 750 751
		});
	}

	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__
752
						"settingsEditor.searchError" : {
R
Rob Lourens 已提交
753 754 755 756 757 758 759
							"message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" },
							"filter": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
						}
					*/
					const message = getErrorMessage(err).trim();
					if (message && message !== 'Error') {
						// "Error" = any generic network error
760
						this.telemetryService.publicLog('settingsEditor.searchError', { message, filter });
R
Rob Lourens 已提交
761 762 763 764 765 766 767
						this.logService.info('Setting search error: ' + message);
					}
					return null;
				}
			});
	}

768
	private layoutTrees(dimension: DOM.Dimension): void {
769
		const listHeight = dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 11 /*padding*/);
770 771 772
		const settingsTreeHeight = listHeight - 14;
		this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`;
		this.settingsTree.layout(settingsTreeHeight, 800);
773

774 775 776 777 778
		const selectedSetting = this.settingsTree.getSelection()[0];
		if (selectedSetting) {
			this.settingsTree.refresh(selectedSetting);
		}

779 780 781
		const tocTreeHeight = listHeight - 16;
		this.tocTreeContainer.style.height = `${tocTreeHeight}px`;
		this.tocTree.layout(tocTreeHeight, 175);
782
	}
R
Rob Lourens 已提交
783
}
784

785 786 787 788
interface ISettingsToolbarContext {
	target: SettingsTarget;
}

789 790
class OpenSettingsAction extends Action {
	static readonly ID = 'settings.openSettingsJson';
791
	static readonly LABEL = localize('openSettingsJsonLabel', "Open settings.json");
792 793 794 795 796 797 798 799

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


800
	run(context?: ISettingsToolbarContext): TPromise<void> {
801 802 803 804
		return this._run(context)
			.then(() => { });
	}

805 806 807
	private _run(context?: ISettingsToolbarContext): TPromise<any> {
		const target = context && context.target;
		if (target === ConfigurationTarget.USER) {
808
			return this.preferencesService.openGlobalSettings();
809
		} else if (target === ConfigurationTarget.WORKSPACE) {
810
			return this.preferencesService.openWorkspaceSettings();
811 812
		} else if (URI.isUri(target)) {
			return this.preferencesService.openFolderSettings(target);
813 814 815 816 817 818
		}

		return TPromise.wrap(null);
	}
}

819 820
class FilterByTagAction extends Action {
	static readonly ID = 'settings.filterByTag';
821 822 823 824

	constructor(
		label: string,
		private tag: string,
825
		private settingsEditor: SettingsEditor2
826
	) {
827
		super(FilterByTagAction.ID, label, 'toggle-filter-tag');
828 829 830
	}

	run(): TPromise<void> {
831
		this.settingsEditor.search(this.tag === MODIFIED_SETTING_TAG ? `@${this.tag} ` : `@tag:${this.tag} `);
832
		return TPromise.as(null);
833 834
	}
}