settingsEditor2.ts 40.5 KB
Newer Older
R
Rob Lourens 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as DOM from 'vs/base/browser/dom';
7
import { Button } from 'vs/base/browser/ui/button/button';
R
Rob Lourens 已提交
8 9 10 11 12
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox';
import { IAction } from 'vs/base/common/actions';
13
import * as arrays from 'vs/base/common/arrays';
R
Rob Lourens 已提交
14
import { Delayer, ThrottledDelayer } from 'vs/base/common/async';
R
Rob Lourens 已提交
15
import { Color } from 'vs/base/common/color';
16
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
R
Rob Lourens 已提交
17
import { Emitter, Event } from 'vs/base/common/event';
18
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
19
import * as objects from 'vs/base/common/objects';
R
Rob Lourens 已提交
20 21 22 23 24 25
import { TPromise } from 'vs/base/common/winjs.base';
import 'vs/css!./media/settingsEditor2';
import { localize } from 'vs/nls';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
26
import { IEditor } from 'vs/platform/editor/common/editor';
R
Rob Lourens 已提交
27 28
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
29
import { ILogService } from 'vs/platform/log/common/log';
R
Rob Lourens 已提交
30
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
31 32
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
33
import { IThemeService, registerThemingParticipant, ICssStyleCollector, ITheme } from 'vs/platform/theme/common/themeService';
R
Rob Lourens 已提交
34 35
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorOptions } from 'vs/workbench/common/editor';
36
import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets';
37
import { IPreferencesService, ISearchResult, ISetting, 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';
40 41
import { IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences';
import { KeyCode } from 'vs/base/common/keyCodes';
R
Rob Lourens 已提交
42 43 44

const SETTINGS_ENTRY_TEMPLATE_ID = 'settings.entry.template';
const SETTINGS_GROUP_ENTRY_TEMPLATE_ID = 'settings.group.template';
45 46 47
const BUTTON_ROW_ENTRY_TEMPLATE = 'settings.buttonRow.template';

const ALL_SETTINGS_BUTTON_ID = 'allSettings';
R
Rob Lourens 已提交
48 49 50 51 52 53 54

interface IListEntry {
	id: string;
	templateId: string;
}

interface ISettingItemEntry extends IListEntry {
55
	templateId: typeof SETTINGS_ENTRY_TEMPLATE_ID;
R
Rob Lourens 已提交
56 57 58 59 60
	key: string;
	value: any;
	isConfigured: boolean;
	description: string;
	overriddenScopeList: string[];
61 62
	isExpanded: boolean;
	isExpandable?: boolean;
63
	isFocused?: boolean;
R
Rob Lourens 已提交
64 65 66 67
	type?: string | string[];
	enum?: string[];
}

68 69 70 71 72 73
enum ExpandState {
	Expanded,
	Collapsed,
	NA
}

74
interface IGroupTitleEntry extends IListEntry {
75
	templateId: typeof SETTINGS_GROUP_ENTRY_TEMPLATE_ID;
R
Rob Lourens 已提交
76
	title: string;
77
	expandState: ExpandState;
R
Rob Lourens 已提交
78 79
}

80
interface IButtonRowEntry extends IListEntry {
81
	templateId: typeof BUTTON_ROW_ENTRY_TEMPLATE;
82
	label: string;
R
Rob Lourens 已提交
83 84
}

85 86
type ListEntry = ISettingItemEntry | IGroupTitleEntry | IButtonRowEntry;

R
Rob Lourens 已提交
87 88 89 90 91
enum SearchResultIdx {
	Local = 0,
	Remote = 1
}

92 93
const $ = DOM.$;

94
export const modifiedItemForeground = registerColor('settings.modifiedItemForeground', {
95 96 97
	light: '#019001',
	dark: '#73C991',
	hc: '#73C991'
98
}, localize('modifiedItemForeground', "The foreground color for a modified setting."));
R
Rob Lourens 已提交
99 100 101 102 103 104 105 106 107 108 109

export class SettingsEditor2 extends BaseEditor {

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

	private defaultSettingsEditorModel: DefaultSettingsEditorModel;

	private headerContainer: HTMLElement;
	private searchWidget: SearchWidget;
	private settingsTargetsWidget: SettingsTargetsWidget;

110
	private showConfiguredSettingsOnlyCheckbox: HTMLInputElement;
R
Rob Lourens 已提交
111 112

	private settingsListContainer: HTMLElement;
113
	private settingsList: List<ListEntry>;
R
Rob Lourens 已提交
114 115 116 117

	private dimension: DOM.Dimension;
	private searchFocusContextKey: IContextKey<boolean>;

118
	private delayedModifyLogging: Delayer<void>;
R
Rob Lourens 已提交
119 120 121 122 123 124 125
	private delayedFilterLogging: Delayer<void>;
	private localSearchDelayer: Delayer<void>;
	private remoteSearchThrottle: ThrottledDelayer<void>;

	private currentLocalSearchProvider: ISearchProvider;
	private currentRemoteSearchProvider: ISearchProvider;

126
	private pendingSettingModifiedReport: { key: string, value: any };
R
Rob Lourens 已提交
127

128 129
	// <TODO@roblou> factor out tree/list viewmodel to somewhere outside this class
	private searchResultModel: SearchResultModel;
130
	private groupExpanded = new Map<string, boolean>();
131
	private itemExpanded = new Map<string, boolean>();
132 133
	private showConfiguredSettingsOnly = false;
	private showAllSettings = false;
134
	private inRender = false;
135
	// </TODO>
136

R
Rob Lourens 已提交
137 138 139 140 141 142
	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
		@IConfigurationService private configurationService: IConfigurationService,
		@IThemeService themeService: IThemeService,
		@IContextMenuService contextMenuService: IContextMenuService,
		@IPreferencesService private preferencesService: IPreferencesService,
R
Rob Lourens 已提交
143 144 145
		@IInstantiationService private instantiationService: IInstantiationService,
		@IPreferencesSearchService private preferencesSearchService: IPreferencesSearchService,
		@ILogService private logService: ILogService
R
Rob Lourens 已提交
146 147
	) {
		super(SettingsEditor2.ID, telemetryService, themeService);
148
		this.delayedModifyLogging = new Delayer<void>(1000);
R
Rob Lourens 已提交
149 150 151
		this.delayedFilterLogging = new Delayer<void>(1000);
		this.localSearchDelayer = new Delayer(100);
		this.remoteSearchThrottle = new ThrottledDelayer(200);
152
		this.searchResultModel = new SearchResultModel();
R
Rob Lourens 已提交
153

154
		this._register(configurationService.onDidChangeConfiguration(() => this.renderEntries()));
R
Rob Lourens 已提交
155 156 157 158 159 160 161 162 163
	}

	createEditor(parent: HTMLElement): void {
		const prefsEditorElement = DOM.append(parent, $('div', { class: 'settings-editor' }));

		this.createHeader(prefsEditorElement);
		this.createBody(prefsEditorElement);
	}

164
	setInput(input: SettingsEditor2Input, options: EditorOptions): TPromise<void> {
R
Rob Lourens 已提交
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
		const oldInput = this.input;
		return super.setInput(input)
			.then(() => {
				if (!input.matches(oldInput)) {
					this.render();
				}
			});
	}

	clearInput(): void {
		super.clearInput();
	}

	layout(dimension: DOM.Dimension): void {
		this.dimension = dimension;
		this.searchWidget.layout(dimension);

		this.layoutSettingsList();
		this.render();
	}

	focus(): void {
		this.searchWidget.focus();
	}

	getSecondaryActions(): IAction[] {
		return <IAction[]>[
		];
	}

	search(filter: string): void {
		this.searchWidget.focus();
	}

	clearSearchResults(): void {
		this.searchWidget.clear();
	}

	private createHeader(parent: HTMLElement): void {
		this.headerContainer = DOM.append(parent, $('.settings-header'));

206
		const previewHeader = DOM.append(this.headerContainer, $('.settings-preview-header'));
R
Rob Lourens 已提交
207 208 209 210 211 212

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

		const previewTextLabel = DOM.append(previewHeader, $('span.settings-preview-label'));
		previewTextLabel.textContent = localize('previewLabel', "This is a preview of our new settings editor. You can also ");
213 214 215 216 217 218
		const openSettingsButton = this._register(new Button(previewHeader, { title: true, buttonBackground: null, buttonHoverBackground: null }));
		this._register(attachButtonStyler(openSettingsButton, this.themeService, {
			buttonBackground: Color.transparent.toString(),
			buttonHoverBackground: Color.transparent.toString(),
			buttonForeground: 'foreground'
		}));
R
Rob Lourens 已提交
219
		openSettingsButton.label = localize('openSettingsLabel', "open the original editor.");
220 221 222 223
		openSettingsButton.element.classList.add('open-settings-button');

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

R
Rob Lourens 已提交
224 225 226 227 228 229
		const searchContainer = DOM.append(this.headerContainer, $('.search-container'));
		this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, {
			ariaLabel: localize('SearchSettings.AriaLabel', "Search settings"),
			placeholder: localize('SearchSettings.Placeholder', "Search settings"),
			focusKey: this.searchFocusContextKey
		}));
R
Rob Lourens 已提交
230
		this._register(this.searchWidget.onDidChange(() => this.onInputChanged()));
231 232 233 234 235 236
		this._register(DOM.addStandardDisposableListener(this.searchWidget.domNode, 'keydown', e => {
			if (e.keyCode === KeyCode.DownArrow) {
				this.settingsList.focusFirst();
				this.settingsList.domFocus();
			}
		}));
R
Rob Lourens 已提交
237

238
		const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
R
Rob Lourens 已提交
239 240 241 242 243
		const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
		this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer));
		this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER;
		this.settingsTargetsWidget.onDidTargetChange(e => this.renderEntries());

244
		this.createHeaderControls(headerControlsContainer);
R
Rob Lourens 已提交
245 246
	}

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

250 251 252
		this.showConfiguredSettingsOnlyCheckbox = DOM.append(headerControlsContainerRight, $('input#configured-only-checkbox'));
		this.showConfiguredSettingsOnlyCheckbox.type = 'checkbox';
		const showConfiguredSettingsOnlyLabel = <HTMLLabelElement>DOM.append(headerControlsContainerRight, $('label.configured-only-label'));
253
		showConfiguredSettingsOnlyLabel.textContent = localize('showOverriddenOnly', "Show modified only");
254
		showConfiguredSettingsOnlyLabel.htmlFor = 'configured-only-checkbox';
255

256
		this._register(DOM.addDisposableListener(this.showConfiguredSettingsOnlyCheckbox, 'change', e => this.onShowConfiguredOnlyClicked()));
257 258 259 260 261 262 263 264 265 266 267 268
	}

	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 已提交
269 270 271 272 273 274 275 276 277 278 279
	}

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

		this.createList(bodyContainer);
	}

	private createList(parent: HTMLElement): void {
		this.settingsListContainer = DOM.append(parent, $('.settings-list-container'));

280
		const settingItemRenderer = this.instantiationService.createInstance(SettingItemRenderer, this.settingsListContainer);
281
		this._register(settingItemRenderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value)));
282
		this._register(settingItemRenderer.onDidOpenSettings(() => this.openSettingsFile()));
283
		this._register(settingItemRenderer.onDidToggleExpandSetting(e => this.toggleSettingExpanded(e)));
284 285

		const buttonItemRenderer = new ButtonRowRenderer();
286
		this._register(buttonItemRenderer.onDidClick(e => this.onShowAllSettingsToggled()));
287

288
		const groupTitleRenderer = new GroupTitleRenderer();
289
		this._register(groupTitleRenderer.onDidClickGroup(id => this.toggleGroupExpanded(id)));
290

R
Rob Lourens 已提交
291 292 293
		this.settingsList = this._register(this.instantiationService.createInstance(
			WorkbenchList,
			this.settingsListContainer,
294
			new SettingItemDelegate(this.settingsListContainer),
295
			[settingItemRenderer, groupTitleRenderer, buttonItemRenderer],
R
Rob Lourens 已提交
296 297
			{
				identityProvider: e => e.id,
298
				ariaLabel: localize('settingsListLabel', "Settings")
R
Rob Lourens 已提交
299
			})
300
		) as WorkbenchList<ListEntry>;
R
Rob Lourens 已提交
301

302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
		this._register(this.settingsList.onDidFocus(() => {
			DOM.addClass(this.settingsList.getHTMLElement(), 'focused');
		}));
		this._register(this.settingsList.onDidBlur(() => {
			DOM.removeClass(this.settingsList.getHTMLElement(), 'focused');
		}));

		this.settingsList.onOpen(e => {
			const entry = e.elements[0];
			if (!entry) {
				return;
			}

			if (entry.templateId === SETTINGS_GROUP_ENTRY_TEMPLATE_ID) {
				this.toggleGroupExpanded(entry.id);
			} else if (entry.templateId === SETTINGS_ENTRY_TEMPLATE_ID) {
				this.toggleSettingExpanded(entry);
			} else if (entry.templateId === BUTTON_ROW_ENTRY_TEMPLATE) {
				this.onShowAllSettingsToggled();
			}
		});
323 324 325 326 327 328

		this._register(this.settingsList.onFocusChange(e => {
			if (!this.inRender) {
				this.renderEntries();
			}
		}));
329 330 331 332 333 334 335 336 337 338 339 340 341
	}

	private toggleGroupExpanded(id: string): void {
		const isExpanded = !!this.groupExpanded.get(id);
		this.groupExpanded.set(id, !isExpanded);
		this.renderEntries();
	}

	private toggleSettingExpanded(entry: ISettingItemEntry): void {
		if (entry.isExpandable) {
			this.itemExpanded.set(entry.key, !entry.isExpanded);
			this.renderEntries();
		}
R
Rob Lourens 已提交
342 343
	}

344
	private onShowAllSettingsToggled(): void {
345 346 347 348
		this.showAllSettings = !this.showAllSettings;
		this.render();
	}

349
	private onShowConfiguredOnlyClicked(): void {
350
		this.showConfiguredSettingsOnly = this.showConfiguredSettingsOnlyCheckbox.checked;
351 352 353
		this.render();
	}

R
Rob Lourens 已提交
354
	private onDidChangeSetting(key: string, value: any): void {
355 356 357 358
		// 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
		this.configurationService.updateValue(key, value, <ConfigurationTarget>this.settingsTargetsWidget.settingsTarget)
			.then(() => this.renderEntries());
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419

		const reportModifiedProps = {
			key,
			query: this.searchWidget.getValue(),
			searchResults: this.searchResultModel.getUniqueResults(),
			rawResults: this.searchResultModel.getRawResults(),
			showConfiguredOnly: this.showConfiguredSettingsOnly,
			isReset: typeof value === 'undefined',
			settingsTarget: this.settingsTargetsWidget.settingsTarget as SettingsTarget
		};

		if (this.pendingSettingModifiedReport && key !== this.pendingSettingModifiedReport.key) {
			this.reportModifiedSetting(reportModifiedProps);
		}

		this.pendingSettingModifiedReport = { key, value };
		this.delayedModifyLogging.trigger(() => this.reportModifiedSetting(reportModifiedProps));
	}

	private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[], rawResults: ISearchResult[], showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void {
		this.pendingSettingModifiedReport = null;

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

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

		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__
420
			"settingsEditor.settingModified" : {
421 422 423 424 425 426 427 428 429 430
				"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" }
			}
		*/
431
		this.telemetryService.publicLog('settingsEditor.settingModified', data);
R
Rob Lourens 已提交
432 433 434 435 436 437 438 439 440 441 442
	}

	private render(): TPromise<any> {
		if (this.input) {
			return this.input.resolve()
				.then((model: DefaultSettingsEditorModel) => this.defaultSettingsEditorModel = model)
				.then(() => this.renderEntries());
		}
		return TPromise.as(null);
	}

R
Rob Lourens 已提交
443 444 445
	private onInputChanged(): void {
		const query = this.searchWidget.getValue().trim();
		this.delayedFilterLogging.cancel();
446
		this.triggerSearch(query).then(() => {
447 448
			if (query && this.searchResultModel.hasResults()) {
				this.delayedFilterLogging.trigger(() => this.reportFilteringUsed(query, this.searchResultModel.getUniqueResults()));
449 450
			}
		});
R
Rob Lourens 已提交
451 452 453 454 455 456
	}

	private triggerSearch(query: string): TPromise<void> {
		if (query) {
			return TPromise.join([
				this.localSearchDelayer.trigger(() => this.localFilterPreferences(query)),
457
				this.remoteSearchThrottle.trigger(() => this.remoteSearchPreferences(query), 500)
R
Rob Lourens 已提交
458 459 460 461 462 463
			]) as TPromise;
		} else {
			// When clearing the input, update immediately to clear it
			this.localSearchDelayer.cancel();
			this.remoteSearchThrottle.cancel();

464
			this.searchResultModel.clear();
R
Rob Lourens 已提交
465 466 467 468 469
			this.renderEntries();
			return TPromise.wrap(null);
		}
	}

470 471 472 473 474 475 476 477 478 479 480
	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;
481 482
		if (nlpResult) {
			counts['nlpResult'] = nlpResult.filterMatches.length;
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
		}

		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 }
			}
		*/
503
		this.telemetryService.publicLog('settingsEditor.filter', data);
504 505
	}

R
Rob Lourens 已提交
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
	private localFilterPreferences(query: string): TPromise<void> {
		this.currentLocalSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Local, this.currentLocalSearchProvider);
	}

	private remoteSearchPreferences(query: string): TPromise<void> {
		this.currentRemoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query);
		return this.filterOrSearchPreferences(query, SearchResultIdx.Remote, this.currentRemoteSearchProvider);
	}

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

		return TPromise.join(filterPs).then(results => {
			const [result] = results;
521
			this.searchResultModel.setResult(type, result);
522
			return this.render();
R
Rob Lourens 已提交
523 524 525 526 527 528 529 530 531 532 533
		});
	}

	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__
534
						"settingsEditor.searchError" : {
R
Rob Lourens 已提交
535 536 537 538 539 540 541
							"message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" },
							"filter": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
						}
					*/
					const message = getErrorMessage(err).trim();
					if (message && message !== 'Error') {
						// "Error" = any generic network error
542
						this.telemetryService.publicLog('settingsEditor.searchError', { message, filter });
R
Rob Lourens 已提交
543 544 545 546 547 548 549
						this.logService.info('Setting search error: ' + message);
					}
					return null;
				}
			});
	}

550
	private getEntriesFromSearch(searchResults: ISearchResult[]): ListEntry[] {
R
Rob Lourens 已提交
551 552 553
		const entries: ISettingItemEntry[] = [];
		const seenSettings = new Set<string>();

554 555
		const focusedElement = this.settingsList.getFocusedElements()[0];
		const focusedId = focusedElement && focusedElement.id;
R
Rob Lourens 已提交
556 557 558 559 560 561 562
		for (let result of searchResults) {
			if (!result) {
				continue;
			}

			for (let match of result.filterMatches) {
				if (!seenSettings.has(match.setting.key)) {
563 564 565
					const entry = this.settingToEntry(match.setting, 'search');
					entry.isFocused = entry.id === focusedId;

R
Rob Lourens 已提交
566 567 568 569 570 571 572 573
					if (!this.showConfiguredSettingsOnly || entry.isConfigured) {
						seenSettings.add(entry.key);
						entries.push(entry);
					}
				}
			}
		}

574
		return entries;
R
Rob Lourens 已提交
575 576 577
	}

	private renderEntries(): void {
578
		this.inRender = true;
579 580 581
		if (!this.defaultSettingsEditorModel) {
			return;
		}
R
Rob Lourens 已提交
582

583 584
		const focusedRowItem = DOM.findParentWithClass(<HTMLElement>document.activeElement, 'monaco-list-row');
		const focusedRowId = focusedRowItem && focusedRowItem.id;
R
Rob Lourens 已提交
585

586 587
		const entries = this.searchResultModel.hasResults() ?
			this.getEntriesFromSearch(this.searchResultModel.getUniqueResults()) :
588 589 590 591 592 593 594 595 596 597 598 599 600
			this.getEntriesFromModel();

		this.settingsList.splice(0, this.settingsList.length, entries);

		// Hack to restore the same focused element after editing.
		// TODO@roblou figure out the whole keyboard navigation story
		if (focusedRowId) {
			const rowSelector = `.monaco-list-row#${focusedRowId}`;
			const inputElementToFocus: HTMLElement = this.settingsListContainer.querySelector(`${rowSelector} input, ${rowSelector} select`);
			if (inputElementToFocus) {
				inputElementToFocus.focus();
			}
		}
601 602

		this.inRender = false;
603 604
	}

605 606
	private getEntriesFromModel(): ListEntry[] {
		const entries: ListEntry[] = [];
607 608 609
		const focusedElement = this.settingsList.getFocusedElements()[0];
		const focusedId = focusedElement && focusedElement.id;

610
		for (let groupIdx = 0; groupIdx < this.defaultSettingsEditorModel.settingsGroups.length; groupIdx++) {
611
			if (groupIdx > 0 && !this.showAllSettings && !this.showConfiguredSettingsOnly) {
612 613 614 615
				break;
			}

			const group = this.defaultSettingsEditorModel.settingsGroups[groupIdx];
616 617
			const isExpanded = groupIdx === 0 || this.groupExpanded.get(group.id);

618
			const groupEntries = [];
619 620 621
			if (isExpanded) {
				for (const section of group.sections) {
					for (const setting of section.settings) {
622 623
						const entry = this.settingToEntry(setting, group.id);
						entry.isFocused = entry.id === focusedId;
624

625 626 627
						if (!this.showConfiguredSettingsOnly || entry.isConfigured) {
							groupEntries.push(entry);
						}
R
Rob Lourens 已提交
628 629
					}
				}
630
			}
R
Rob Lourens 已提交
631

632 633 634 635 636
			if (!isExpanded || groupEntries.length) {
				const expandState = groupIdx === 0 ? ExpandState.NA :
					isExpanded ? ExpandState.Expanded :
						ExpandState.Collapsed;

637 638 639
				entries.push(<IGroupTitleEntry>{
					id: group.id,
					templateId: SETTINGS_GROUP_ENTRY_TEMPLATE_ID,
640 641
					title: group.title,
					expandState
642
				});
R
Rob Lourens 已提交
643

644 645
				entries.push(...groupEntries);
			}
646

647
			if (groupIdx === 0 && !this.showConfiguredSettingsOnly) {
648 649 650 651 652 653 654 655
				const showAllSettingsLabel = this.showAllSettings ?
					localize('showFewerSettingsLabel', "Show Fewer Settings") :
					localize('showAllSettingsLabel', "Show All Settings");
				entries.push(<IButtonRowEntry>{
					id: ALL_SETTINGS_BUTTON_ID,
					label: showAllSettingsLabel,
					templateId: BUTTON_ROW_ENTRY_TEMPLATE
				});
R
Rob Lourens 已提交
656
			}
657
		}
R
Rob Lourens 已提交
658

659
		return entries;
R
Rob Lourens 已提交
660 661
	}

662
	private settingToEntry(s: ISetting, groupId: string): ISettingItemEntry {
R
Rob Lourens 已提交
663 664 665 666 667 668 669 670 671 672 673 674 675
		const targetSelector = this.settingsTargetsWidget.settingsTarget === ConfigurationTarget.USER ? 'user' : 'workspace';
		const inspected = this.configurationService.inspect(s.key);
		const isConfigured = typeof inspected[targetSelector] !== 'undefined';
		const displayValue = isConfigured ? inspected[targetSelector] : inspected.default;
		const overriddenScopeList = [];
		if (targetSelector === 'user' && typeof inspected.workspace !== 'undefined') {
			overriddenScopeList.push('Workspace');
		}

		if (targetSelector === 'workspace' && typeof inspected.user !== 'undefined') {
			overriddenScopeList.push('User');
		}

676 677
		const isExpanded = !!this.itemExpanded.get(s.key);

R
Rob Lourens 已提交
678
		return <ISettingItemEntry>{
679
			id: `${groupId}_${s.key}`,
R
Rob Lourens 已提交
680 681 682 683 684 685 686
			key: s.key,
			value: displayValue,
			isConfigured,
			overriddenScopeList,
			description: s.description.join('\n'),
			enum: s.enum,
			type: s.type,
687 688
			templateId: SETTINGS_ENTRY_TEMPLATE_ID,
			isExpanded
R
Rob Lourens 已提交
689 690 691 692 693 694 695 696 697 698
		};
	}

	private layoutSettingsList(): void {
		const listHeight = this.dimension.height - (DOM.getDomNodePagePosition(this.headerContainer).height + 12 /*padding*/);
		this.settingsListContainer.style.height = `${listHeight}px`;
		this.settingsList.layout(listHeight);
	}
}

699 700 701
class SettingItemDelegate implements IDelegate<ListEntry> {

	constructor(private measureContainer: HTMLElement) {
R
Rob Lourens 已提交
702

703 704 705
	}

	getHeight(entry: ListEntry) {
R
Rob Lourens 已提交
706
		if (entry.templateId === SETTINGS_GROUP_ENTRY_TEMPLATE_ID) {
707
			return 30;
R
Rob Lourens 已提交
708 709 710
		}

		if (entry.templateId === SETTINGS_ENTRY_TEMPLATE_ID) {
711 712 713 714 715
			if (entry.isExpanded) {
				return this.getDynamicHeight(entry);
			} else {
				return 68;
			}
R
Rob Lourens 已提交
716 717
		}

718 719 720 721
		if (entry.templateId === BUTTON_ROW_ENTRY_TEMPLATE) {
			return 60;
		}

R
Rob Lourens 已提交
722 723 724
		return 0;
	}

725
	getTemplateId(element: ListEntry) {
R
Rob Lourens 已提交
726 727
		return element.templateId;
	}
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742

	private getDynamicHeight(entry: ISettingItemEntry): number {
		return measureSettingItemEntry(entry, this.measureContainer);
	}
}

function measureSettingItemEntry(entry: ISettingItemEntry, measureContainer: HTMLElement): number {
	const measureHelper = DOM.append(measureContainer, $('.setting-item-measure-helper.monaco-list-row'));

	const template = SettingItemRenderer.renderTemplate(measureHelper);
	SettingItemRenderer.renderElement(entry, 0, template);

	const height = measureHelper.offsetHeight;
	measureContainer.removeChild(measureHelper);
	return height;
R
Rob Lourens 已提交
743 744
}

745
interface IDisposableTemplate {
R
Rob Lourens 已提交
746
	toDispose: IDisposable[];
747 748 749 750
}

interface ISettingItemTemplate extends IDisposableTemplate {
	parent: HTMLElement;
R
Rob Lourens 已提交
751

752
	context?: ISettingItemEntry;
R
Rob Lourens 已提交
753
	containerElement: HTMLElement;
754
	categoryElement: HTMLElement;
R
Rob Lourens 已提交
755 756
	labelElement: HTMLElement;
	descriptionElement: HTMLElement;
757
	showMoreElement: HTMLElement;
R
Rob Lourens 已提交
758 759 760 761
	valueElement: HTMLElement;
	overridesElement: HTMLElement;
}

762 763
interface IGroupTitleTemplate extends IDisposableTemplate {
	context?: IGroupTitleEntry;
R
Rob Lourens 已提交
764 765 766 767
	parent: HTMLElement;
	labelElement: HTMLElement;
}

768
interface IButtonRowTemplate extends IDisposableTemplate {
R
Rob Lourens 已提交
769 770
	parent: HTMLElement;

771 772
	button: Button;
	entry?: IButtonRowEntry;
R
Rob Lourens 已提交
773 774 775 776 777 778 779 780 781 782 783 784
}

interface ISettingChangeEvent {
	key: string;
	value: any; // undefined => reset unconfigure
}

class SettingItemRenderer implements IRenderer<ISettingItemEntry, ISettingItemTemplate> {

	private readonly _onDidChangeSetting: Emitter<ISettingChangeEvent> = new Emitter<ISettingChangeEvent>();
	public readonly onDidChangeSetting: Event<ISettingChangeEvent> = this._onDidChangeSetting.event;

785 786 787
	private readonly _onDidOpenSettings: Emitter<void> = new Emitter<void>();
	public readonly onDidOpenSettings: Event<void> = this._onDidOpenSettings.event;

788 789
	private readonly _onDidToggleExpandSetting: Emitter<ISettingItemEntry> = new Emitter<ISettingItemEntry>();
	public readonly onDidToggleExpandSetting: Event<ISettingItemEntry> = this._onDidToggleExpandSetting.event;
790

R
Rob Lourens 已提交
791 792 793
	get templateId(): string { return SETTINGS_ENTRY_TEMPLATE_ID; }

	constructor(
794
		private measureContainer: HTMLElement,
R
Rob Lourens 已提交
795 796 797 798 799
		@IContextViewService private contextViewService: IContextViewService,
		@IThemeService private themeService: IThemeService
	) { }

	renderTemplate(parent: HTMLElement): ISettingItemTemplate {
800 801 802 803
		return SettingItemRenderer.renderTemplate(parent, this);
	}

	static renderTemplate(parent: HTMLElement, that?: SettingItemRenderer): ISettingItemTemplate {
R
Rob Lourens 已提交
804 805
		DOM.addClass(parent, 'setting-item');

806
		const itemContainer = DOM.append(parent, $('.setting-item-container'));
R
Rob Lourens 已提交
807 808 809
		const leftElement = DOM.append(itemContainer, $('.setting-item-left'));
		const rightElement = DOM.append(itemContainer, $('.setting-item-right'));

810
		const titleElement = DOM.append(leftElement, $('.setting-item-title'));
811
		const categoryElement = DOM.append(titleElement, $('span.setting-item-category'));
R
Rob Lourens 已提交
812
		const labelElement = DOM.append(titleElement, $('span.setting-item-label'));
813 814
		const overridesElement = DOM.append(titleElement, $('span.setting-item-overrides'));
		const descriptionElement = DOM.append(leftElement, $('.setting-item-description'));
815
		const showMoreElement = DOM.append(leftElement, $('.setting-show-more'));
R
Rob Lourens 已提交
816

817
		const valueElement = DOM.append(rightElement, $('.setting-item-value'));
R
Rob Lourens 已提交
818

819 820
		const toDispose = [];
		const template: ISettingItemTemplate = {
R
Rob Lourens 已提交
821
			parent: parent,
822
			toDispose,
R
Rob Lourens 已提交
823 824

			containerElement: itemContainer,
825
			categoryElement,
R
Rob Lourens 已提交
826 827
			labelElement,
			descriptionElement,
828
			showMoreElement,
R
Rob Lourens 已提交
829 830 831
			valueElement,
			overridesElement
		};
832 833 834 835 836

		if (that) {
			toDispose.push(DOM.addDisposableListener(descriptionElement, 'click', () => {
				const entry = template.context;
				if (entry && entry.isExpandable) {
837
					that._onDidToggleExpandSetting.fire(entry);
838 839 840 841 842
				}
			}));
		}

		return template;
R
Rob Lourens 已提交
843 844 845
	}

	renderElement(entry: ISettingItemEntry, index: number, template: ISettingItemTemplate): void {
846 847 848 849 850
		return SettingItemRenderer.renderElement(entry, index, template, this);
	}

	static renderElement(entry: ISettingItemEntry, index: number, template: ISettingItemTemplate, that?: SettingItemRenderer): void {
		template.context = entry;
R
Rob Lourens 已提交
851
		DOM.toggleClass(template.parent, 'odd', index % 2 === 1);
852 853
		DOM.toggleClass(template.parent, 'is-configured', entry.isConfigured);
		DOM.toggleClass(template.parent, 'is-expanded', entry.isExpanded);
R
Rob Lourens 已提交
854

855
		const titleTooltip = entry.key;
856 857
		const settingKeyDisplay = settingKeyToDisplayFormat(entry.key);
		template.categoryElement.textContent = settingKeyDisplay.category + ': ';
R
Rob Lourens 已提交
858
		template.categoryElement.title = titleTooltip;
859 860

		template.labelElement.textContent = settingKeyDisplay.label;
R
Rob Lourens 已提交
861
		template.labelElement.title = titleTooltip;
R
Rob Lourens 已提交
862
		template.descriptionElement.textContent = entry.description;
863
		template.descriptionElement.title = entry.description;
R
Rob Lourens 已提交
864

865 866 867 868 869 870 871 872 873
		if (that) {
			const expandedHeight = measureSettingItemEntry(entry, that.measureContainer);
			entry.isExpandable = expandedHeight > 68;
			DOM.toggleClass(template.parent, 'is-expandable', entry.isExpandable);
		}

		if (that) {
			that.renderValue(entry, template);
		}
R
Rob Lourens 已提交
874 875

		const resetButton = new Button(template.valueElement);
R
Rob Lourens 已提交
876
		resetButton.element.title = localize('resetButtonTitle', "Reset");
R
Rob Lourens 已提交
877
		resetButton.element.classList.add('setting-reset-button');
878
		resetButton.element.tabIndex = entry.isFocused ? 0 : -1;
879 880 881 882 883 884 885 886 887 888 889 890 891

		if (that) {
			attachButtonStyler(resetButton, that.themeService, {
				buttonBackground: Color.transparent.toString(),
				buttonHoverBackground: Color.transparent.toString()
			});
		}

		if (that) {
			template.toDispose.push(resetButton.onDidClick(e => {
				that._onDidChangeSetting.fire({ key: entry.key, value: undefined });
			}));
		}
R
Rob Lourens 已提交
892 893
		template.toDispose.push(resetButton);

894
		const alsoConfiguredInLabel = localize('alsoConfiguredIn', "Also modified in:");
895 896 897 898 899 900 901
		let overridesElementText = entry.isConfigured ? 'Modified ' : '';

		if (entry.overriddenScopeList.length) {
			overridesElementText = overridesElementText + `(${alsoConfiguredInLabel} ${entry.overriddenScopeList.join(', ')})`;
		}

		template.overridesElement.textContent = overridesElementText;
R
Rob Lourens 已提交
902 903 904 905 906 907 908 909 910 911 912 913 914 915
	}

	private renderValue(entry: ISettingItemEntry, template: ISettingItemTemplate): void {
		const onChange = value => this._onDidChangeSetting.fire({ key: entry.key, value });
		template.valueElement.innerHTML = '';
		if (entry.type === 'string' && entry.enum) {
			this.renderEnum(entry, template, onChange);
		} else if (entry.type === 'boolean') {
			this.renderBool(entry, template, onChange);
		} else if (entry.type === 'string') {
			this.renderText(entry, template, onChange);
		} else if (entry.type === 'number') {
			this.renderText(entry, template, value => onChange(parseInt(value)));
		} else {
916
			this.renderEditInSettingsJson(entry, template);
R
Rob Lourens 已提交
917 918 919 920
		}
	}

	private renderBool(entry: ISettingItemEntry, template: ISettingItemTemplate, onChange: (value: boolean) => void): void {
921
		const checkboxElement = <HTMLInputElement>DOM.append(template.valueElement, $('input.setting-value-checkbox.setting-value-input'));
922 923
		checkboxElement.type = 'checkbox';
		checkboxElement.checked = entry.value;
924
		checkboxElement.tabIndex = entry.isFocused ? 0 : -1;
R
Rob Lourens 已提交
925

926
		template.toDispose.push(DOM.addDisposableListener(checkboxElement, 'change', e => onChange(checkboxElement.checked)));
R
Rob Lourens 已提交
927 928 929 930 931 932
	}

	private renderEnum(entry: ISettingItemEntry, template: ISettingItemTemplate, onChange: (value: string) => void): void {
		const idx = entry.enum.indexOf(entry.value);
		const selectBox = new SelectBox(entry.enum, idx, this.contextViewService);
		template.toDispose.push(selectBox);
933
		template.toDispose.push(attachSelectBoxStyler(selectBox, this.themeService));
R
Rob Lourens 已提交
934
		selectBox.render(template.valueElement);
935
		if (template.valueElement.firstElementChild) {
936
			template.valueElement.firstElementChild.setAttribute('tabindex', entry.isFocused ? '0' : '-1');
937
		}
R
Rob Lourens 已提交
938 939 940 941 942 943 944

		template.toDispose.push(
			selectBox.onDidSelect(e => onChange(entry.enum[e.index])));
	}

	private renderText(entry: ISettingItemEntry, template: ISettingItemTemplate, onChange: (value: string) => void): void {
		const inputBox = new InputBox(template.valueElement, this.contextViewService);
945
		template.toDispose.push(attachInputBoxStyler(inputBox, this.themeService));
R
Rob Lourens 已提交
946 947
		template.toDispose.push(inputBox);
		inputBox.value = entry.value;
948
		inputBox.inputElement.tabIndex = entry.isFocused ? 0 : -1;
R
Rob Lourens 已提交
949 950 951 952 953

		template.toDispose.push(
			inputBox.onDidChange(e => onChange(e)));
	}

954 955 956 957 958
	private renderEditInSettingsJson(entry: ISettingItemEntry, template: ISettingItemTemplate): void {
		const openSettingsButton = new Button(template.valueElement, { title: true, buttonBackground: null, buttonHoverBackground: null });
		openSettingsButton.onDidClick(() => this._onDidOpenSettings.fire());
		openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json");
		openSettingsButton.element.classList.add('edit-in-settings-button');
959 960
		openSettingsButton.element.tabIndex = entry.isFocused ? 0 : -1;

961
		template.toDispose.push(openSettingsButton);
962 963 964 965 966
		template.toDispose.push(attachButtonStyler(openSettingsButton, this.themeService, {
			buttonBackground: Color.transparent.toString(),
			buttonHoverBackground: Color.transparent.toString(),
			buttonForeground: 'foreground'
		}));
967 968
	}

R
Rob Lourens 已提交
969 970 971 972 973
	disposeTemplate(template: ISettingItemTemplate): void {
		dispose(template.toDispose);
	}
}

974
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
975 976 977
	const modifiedItemForegroundColor = theme.getColor(modifiedItemForeground);
	if (modifiedItemForegroundColor) {
		collector.addRule(`.settings-editor > .settings-body > .settings-list-container .monaco-list-row.is-configured .setting-item-title { color: ${modifiedItemForegroundColor}; }`);
978 979 980
	}
});

981
class GroupTitleRenderer implements IRenderer<IGroupTitleEntry, IGroupTitleTemplate> {
R
Rob Lourens 已提交
982

983 984 985 986 987 988
	private static readonly EXPANDED_CLASS = 'settings-group-title-expanded';
	private static readonly COLLAPSED_CLASS = 'settings-group-title-collapsed';

	private readonly _onDidClickGroup: Emitter<string> = new Emitter<string>();
	public readonly onDidClickGroup: Event<string> = this._onDidClickGroup.event;

R
Rob Lourens 已提交
989 990
	get templateId(): string { return SETTINGS_GROUP_ENTRY_TEMPLATE_ID; }

991
	renderTemplate(parent: HTMLElement): IGroupTitleTemplate {
R
Rob Lourens 已提交
992 993
		DOM.addClass(parent, 'group-title');

994
		const labelElement = DOM.append(parent, $('h3.settings-group-title-label'));
995 996 997

		const toDispose = [];
		const template: IGroupTitleTemplate = {
R
Rob Lourens 已提交
998
			parent: parent,
999 1000
			labelElement,
			toDispose
R
Rob Lourens 已提交
1001
		};
1002 1003 1004 1005 1006 1007 1008 1009

		toDispose.push(DOM.addDisposableListener(labelElement, 'click', () => {
			if (template.context) {
				this._onDidClickGroup.fire(template.context.id);
			}
		}));

		return template;
R
Rob Lourens 已提交
1010 1011
	}

1012
	renderElement(entry: IGroupTitleEntry, index: number, template: IGroupTitleTemplate): void {
1013
		template.context = entry;
R
Rob Lourens 已提交
1014
		template.labelElement.textContent = entry.title;
1015 1016 1017 1018 1019 1020 1021 1022 1023

		template.labelElement.classList.remove(GroupTitleRenderer.EXPANDED_CLASS);
		template.labelElement.classList.remove(GroupTitleRenderer.COLLAPSED_CLASS);

		if (entry.expandState === ExpandState.Expanded) {
			template.labelElement.classList.add(GroupTitleRenderer.EXPANDED_CLASS);
		} else if (entry.expandState === ExpandState.Collapsed) {
			template.labelElement.classList.add(GroupTitleRenderer.COLLAPSED_CLASS);
		}
R
Rob Lourens 已提交
1024 1025
	}

1026
	disposeTemplate(template: IGroupTitleTemplate): void {
1027
		dispose(template.toDispose);
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062
	}
}

class ButtonRowRenderer implements IRenderer<IButtonRowEntry, IButtonRowTemplate> {

	private readonly _onDidClick: Emitter<string> = new Emitter<string>();
	public readonly onDidClick: Event<string> = this._onDidClick.event;

	get templateId(): string { return BUTTON_ROW_ENTRY_TEMPLATE; }

	renderTemplate(parent: HTMLElement): IButtonRowTemplate {
		DOM.addClass(parent, 'all-settings');

		const buttonElement = DOM.append(parent, $('.all-settings-button'));

		const button = new Button(buttonElement);
		const toDispose: IDisposable[] = [button];

		const template: IButtonRowTemplate = {
			parent: parent,
			toDispose,

			button
		};
		toDispose.push(button.onDidClick(e => this._onDidClick.fire(template.entry && template.entry.label)));

		return template;
	}

	renderElement(entry: IButtonRowEntry, index: number, template: IButtonRowTemplate): void {
		template.button.label = entry.label;
		template.entry = entry;
	}

	disposeTemplate(template: IButtonRowTemplate): void {
R
Rob Lourens 已提交
1063 1064 1065 1066
		dispose(template.toDispose);
	}
}

1067
export function settingKeyToDisplayFormat(key: string): { category: string, label: string } {
1068
	let label = key
1069
		.replace(/\.([a-z])/g, (match, p1) => `.${p1.toUpperCase()}`)
1070 1071 1072 1073 1074
		.replace(/([a-z])([A-Z])/g, '$1 $2') // fooBar => foo Bar
		.replace(/^[a-z]/g, match => match.toUpperCase()); // foo => Foo

	const lastDotIdx = label.lastIndexOf('.');
	let category = '';
R
Rob Lourens 已提交
1075
	if (lastDotIdx >= 0) {
1076 1077
		category = label.substr(0, lastDotIdx);
		label = label.substr(lastDotIdx + 1);
R
Rob Lourens 已提交
1078 1079
	}

1080
	return { category, label };
R
Rob Lourens 已提交
1081
}
1082

1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129
class SearchResultModel {
	private rawSearchResults: ISearchResult[];
	private cachedUniqueSearchResults: ISearchResult[];

	getUniqueResults(): ISearchResult[] {
		if (this.cachedUniqueSearchResults) {
			return this.cachedUniqueSearchResults;
		}

		if (!this.rawSearchResults) {
			return null;
		}

		const localMatchKeys = new Set();
		const localResult = objects.deepClone(this.rawSearchResults[SearchResultIdx.Local]);
		if (localResult) {
			localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key));
		}

		const remoteResult = objects.deepClone(this.rawSearchResults[SearchResultIdx.Remote]);
		if (remoteResult) {
			remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key));
		}

		this.cachedUniqueSearchResults = [localResult, remoteResult];
		return this.cachedUniqueSearchResults;
	}

	getRawResults(): ISearchResult[] {
		return this.rawSearchResults;
	}

	hasResults(): boolean {
		return !!this.rawSearchResults;
	}

	clear(): void {
		this.cachedUniqueSearchResults = null;
		this.rawSearchResults = null;
	}

	setResult(type: SearchResultIdx, result: ISearchResult): void {
		this.cachedUniqueSearchResults = null;
		this.rawSearchResults = this.rawSearchResults || [];
		this.rawSearchResults[type] = result;
	}
}