quickOpenController.ts 44.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import 'vs/css!./media/quickopen';
J
Johannes Rieken 已提交
9
import { TPromise, ValueCallback } from 'vs/base/common/winjs.base';
10
import * as nls from 'vs/nls';
11
import * as browser from 'vs/base/browser/browser';
12
import * as strings from 'vs/base/common/strings';
13
import URI from 'vs/base/common/uri';
B
Benjamin Pasero 已提交
14
import * as resources from 'vs/base/common/resources';
15
import { defaultGenerator } from 'vs/base/common/idGenerator';
16
import * as types from 'vs/base/common/types';
17
import { Action, IAction } from 'vs/base/common/actions';
18
import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
J
Johannes Rieken 已提交
19 20
import { CancellationToken } from 'vs/base/common/cancellation';
import { Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen';
21
import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup, compareEntries, QuickOpenItemAccessorClass } from 'vs/base/parts/quickopen/browser/quickOpenModel';
J
Johannes Rieken 已提交
22
import { QuickOpenWidget, HideReason } from 'vs/base/parts/quickopen/browser/quickOpenWidget';
23
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
24
import * as labels from 'vs/base/common/labels';
25
import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles';
26
import { Registry } from 'vs/platform/registry/common/platform';
27
import { IResourceInput } from 'vs/platform/editor/common/editor';
J
Johannes Rieken 已提交
28 29 30
import { IModeService } from 'vs/editor/common/services/modeService';
import { getIconClasses } from 'vs/workbench/browser/labels';
import { IModelService } from 'vs/editor/common/services/modelService';
31
import { EditorInput, IWorkbenchEditorConfiguration, IEditorInput } from 'vs/workbench/common/editor';
B
Benjamin Pasero 已提交
32
import { Component } from 'vs/workbench/common/component';
M
Matt Bierner 已提交
33
import { Event, Emitter } from 'vs/base/common/event';
J
Johannes Rieken 已提交
34
import { IPartService } from 'vs/workbench/services/part/common/partService';
35
import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG } from 'vs/workbench/browser/quickopen';
36
import * as errors from 'vs/base/common/errors';
37
import { IPickOpenEntry, IFilePickOpenEntry, IQuickOpenService, IPickOptions, IShowOptions, IPickOpenItem } from 'vs/platform/quickOpen/common/quickOpen';
J
Johannes Rieken 已提交
38 39 40 41 42
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
B
Benjamin Pasero 已提交
43
import { IThemeService } from 'vs/platform/theme/common/themeService';
44
import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/theme';
45 46
import { attachQuickOpenStyler } from 'vs/platform/theme/common/styler';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
B
Benjamin Pasero 已提交
47 48
import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree';
import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
49
import { FileKind, IFileService } from 'vs/platform/files/common/files';
50
import { scoreItem, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
51
import { WorkbenchTree } from 'vs/platform/list/browser/listService';
52 53
import { matchesFuzzyOcticonAware, parseOcticons, IParsedOcticons } from 'vs/base/common/octicon';
import { IMatch } from 'vs/base/common/filters';
54
import { Schemas } from 'vs/base/common/network';
55 56
import Severity from 'vs/base/common/severity';
import { INotificationService } from 'vs/platform/notification/common/notification';
57
import { Dimension, addClass } from 'vs/base/browser/dom';
58
import { INextEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/nextEditorService';
B
Benjamin Pasero 已提交
59
import { INextEditorGroupsService } from 'vs/workbench/services/group/common/nextEditorGroupsService';
E
Erich Gamma 已提交
60 61 62

const HELP_PREFIX = '?';

63
interface IInternalPickOptions {
64
	contextKey?: string;
65
	value?: string;
66
	valueSelection?: [number, number];
67
	placeHolder?: string;
68
	inputDecoration?: Severity;
69 70 71 72 73
	password?: boolean;
	autoFocus?: IAutoFocus;
	matchOnDescription?: boolean;
	matchOnDetail?: boolean;
	ignoreFocusLost?: boolean;
B
Benjamin Pasero 已提交
74
	quickNavigateConfiguration?: IQuickNavigateConfiguration;
75
	onDidType?: (value: string) => any;
E
Erich Gamma 已提交
76 77
}

B
Benjamin Pasero 已提交
78
export class QuickOpenController extends Component implements IQuickOpenService {
E
Erich Gamma 已提交
79

80
	private static readonly MAX_SHORT_RESPONSE_TIME = 500;
81

82
	public _serviceBrand: any;
E
Erich Gamma 已提交
83

84
	private static readonly ID = 'workbench.component.quickopen';
85

M
Matt Bierner 已提交
86 87
	private readonly _onShow: Emitter<void>;
	private readonly _onHide: Emitter<void>;
E
Erich Gamma 已提交
88 89 90 91

	private quickOpenWidget: QuickOpenWidget;
	private pickOpenWidget: QuickOpenWidget;
	private layoutDimensions: Dimension;
92
	private mapResolvedHandlersToPrefix: { [prefix: string]: TPromise<QuickOpenHandler>; };
93
	private mapContextKeyToContext: { [id: string]: IContextKey<boolean>; };
94
	private handlerOnOpenCalled: { [prefix: string]: boolean; };
E
Erich Gamma 已提交
95 96 97 98 99
	private currentResultToken: string;
	private currentPickerToken: string;
	private promisesToCompleteOnHide: ValueCallback[];
	private previousActiveHandlerDescriptor: QuickOpenHandlerDescriptor;
	private actionProvider = new ContributableActionProvider();
100
	private closeOnFocusLost: boolean;
101
	private editorHistoryHandler: EditorHistoryHandler;
E
Erich Gamma 已提交
102 103

	constructor(
104
		@INextEditorService private editorService: INextEditorService,
B
Benjamin Pasero 已提交
105
		@INextEditorGroupsService private editorGroupService: INextEditorGroupsService,
106
		@INotificationService private notificationService: INotificationService,
107
		@IContextKeyService private contextKeyService: IContextKeyService,
108
		@IConfigurationService private configurationService: IConfigurationService,
109
		@IInstantiationService private instantiationService: IInstantiationService,
110
		@IPartService private partService: IPartService,
111
		@IEnvironmentService private environmentService: IEnvironmentService,
B
Benjamin Pasero 已提交
112
		@IThemeService themeService: IThemeService
E
Erich Gamma 已提交
113
	) {
B
Benjamin Pasero 已提交
114
		super(QuickOpenController.ID, themeService);
E
Erich Gamma 已提交
115 116

		this.mapResolvedHandlersToPrefix = {};
117
		this.handlerOnOpenCalled = {};
118
		this.mapContextKeyToContext = {};
E
Erich Gamma 已提交
119 120 121

		this.promisesToCompleteOnHide = [];

122 123
		this.editorHistoryHandler = this.instantiationService.createInstance(EditorHistoryHandler);

124 125
		this._onShow = new Emitter<void>();
		this._onHide = new Emitter<void>();
126

127
		this.updateConfiguration();
128 129 130 131 132

		this.registerListeners();
	}

	private registerListeners(): void {
133
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.updateConfiguration()));
134 135
		this.toUnbind.push(this.partService.onTitleBarVisibilityChange(() => this.positionQuickOpenWidget()));
		this.toUnbind.push(browser.onDidChangeZoomLevel(() => this.positionQuickOpenWidget()));
136 137
	}

138
	private updateConfiguration(): void {
139 140 141
		if (this.environmentService.args['sticky-quickopen']) {
			this.closeOnFocusLost = false;
		} else {
142
			this.closeOnFocusLost = this.configurationService.getValue(CLOSE_ON_FOCUS_LOST_CONFIG);
143
		}
E
Erich Gamma 已提交
144 145
	}

146 147
	public get onShow(): Event<void> {
		return this._onShow.event;
E
Erich Gamma 已提交
148 149
	}

150 151
	public get onHide(): Event<void> {
		return this._onHide.event;
E
Erich Gamma 已提交
152 153
	}

154
	public navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void {
E
Erich Gamma 已提交
155
		if (this.quickOpenWidget) {
156 157 158
			this.quickOpenWidget.navigate(next, quickNavigate);
		}

B
Benjamin Pasero 已提交
159 160
		if (this.pickOpenWidget) {
			this.pickOpenWidget.navigate(next, quickNavigate);
E
Erich Gamma 已提交
161 162 163
		}
	}

164 165 166 167 168
	public pick(picks: TPromise<string[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string>;
	public pick<T extends IPickOpenEntry>(picks: TPromise<T[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string>;
	public pick(picks: string[], options?: IPickOptions, token?: CancellationToken): TPromise<string>;
	public pick<T extends IPickOpenEntry>(picks: T[], options?: IPickOptions, token?: CancellationToken): TPromise<T>;
	public pick(arg1: string[] | TPromise<string[]> | IPickOpenEntry[] | TPromise<IPickOpenEntry[]>, options?: IPickOptions, token?: CancellationToken): TPromise<string | IPickOpenEntry> {
E
Erich Gamma 已提交
169 170 171 172
		if (!options) {
			options = Object.create(null);
		}

173 174 175 176 177 178 179 180 181 182
		let arrayPromise: TPromise<string[] | IPickOpenEntry[]>;
		if (Array.isArray(arg1)) {
			arrayPromise = TPromise.as(arg1);
		} else if (TPromise.is(arg1)) {
			arrayPromise = arg1;
		} else {
			throw new Error('illegal input');
		}

		let isAboutStrings = false;
183
		const entryPromise = arrayPromise.then(elements => {
184 185 186
			return (<Array<string | IPickOpenEntry>>elements).map(element => {
				if (typeof element === 'string') {
					isAboutStrings = true;
187

188 189 190 191 192 193 194
					return <IPickOpenEntry>{ label: element };
				} else {
					return element;
				}
			});
		});

195 196 197 198
		if (this.pickOpenWidget && this.pickOpenWidget.isVisible()) {
			this.pickOpenWidget.hide(HideReason.CANCELED);
		}

199 200 201 202 203 204
		return new TPromise<string | IPickOpenEntry>((resolve, reject, progress) => {

			function onItem(item: IPickOpenEntry): string | IPickOpenEntry {
				return item && isAboutStrings ? item.label : item;
			}

205
			this.doPick(entryPromise, options, token).then(item => resolve(onItem(item)), err => reject(err), item => progress(onItem(item)));
206 207 208
		});
	}

209
	private doPick(picksPromise: TPromise<IPickOpenEntry[]>, options: IInternalPickOptions, token: CancellationToken = CancellationToken.None): TPromise<IPickOpenEntry> {
210
		const autoFocus = options.autoFocus;
E
Erich Gamma 已提交
211 212

		// Use a generated token to avoid race conditions from long running promises
213
		const currentPickerToken = defaultGenerator.nextId();
E
Erich Gamma 已提交
214 215
		this.currentPickerToken = currentPickerToken;

216 217 218
		// Update context
		this.setQuickOpenContextKey(options.contextKey);

E
Erich Gamma 已提交
219 220 221
		// Create upon first open
		if (!this.pickOpenWidget) {
			this.pickOpenWidget = new QuickOpenWidget(
222
				document.getElementById(this.partService.getWorkbenchElementId()),
E
Erich Gamma 已提交
223 224 225
				{
					onOk: () => { /* ignore, handle later */ },
					onCancel: () => { /* ignore, handle later */ },
226
					onType: (value: string) => { /* ignore, handle later */ },
B
Benjamin Pasero 已提交
227 228
					onShow: () => this.handleOnShow(true),
					onHide: (reason) => this.handleOnHide(true, reason)
E
Erich Gamma 已提交
229
				}, {
230
					inputPlaceHolder: options.placeHolder || '',
J
Joao Moreno 已提交
231
					keyboardSupport: false,
232
					treeCreator: (container, config, opts) => this.instantiationService.createInstance(WorkbenchTree, container, config, opts)
233
				}
E
Erich Gamma 已提交
234
			);
235
			this.toUnbind.push(attachQuickOpenStyler(this.pickOpenWidget, this.themeService, { background: SIDE_BAR_BACKGROUND, foreground: SIDE_BAR_FOREGROUND }));
E
Erich Gamma 已提交
236

B
Benjamin Pasero 已提交
237
			const pickOpenContainer = this.pickOpenWidget.create();
238
			addClass(pickOpenContainer, 'show-file-icons');
239
			this.positionQuickOpenWidget();
E
Erich Gamma 已提交
240 241 242 243 244 245 246 247
		}

		// Update otherwise
		else {
			this.pickOpenWidget.setPlaceHolder(options.placeHolder || '');
		}

		// Respect input value
248
		if (options.value) {
249
			this.pickOpenWidget.setValue(options.value, options.valueSelection);
E
Erich Gamma 已提交
250 251 252
		}

		// Respect password
253
		this.pickOpenWidget.setPassword(options.password);
E
Erich Gamma 已提交
254

255 256 257 258 259 260 261
		// Input decoration
		if (!types.isUndefinedOrNull(options.inputDecoration)) {
			this.pickOpenWidget.showInputDecoration(options.inputDecoration);
		} else {
			this.pickOpenWidget.clearInputDecoration();
		}

E
Erich Gamma 已提交
262 263 264 265 266
		// Layout
		if (this.layoutDimensions) {
			this.pickOpenWidget.layout(this.layoutDimensions);
		}

267
		return new TPromise<IPickOpenEntry>((complete, error, progress) => {
268

J
Johannes Rieken 已提交
269 270 271 272 273 274 275
			// Detect cancellation while pick promise is loading
			this.pickOpenWidget.setCallbacks({
				onCancel: () => { complete(void 0); },
				onOk: () => { /* ignore, handle later */ },
				onType: (value: string) => { /* ignore, handle later */ },
			});

276
			// hide widget when being cancelled
277 278 279 280 281
			token.onCancellationRequested(e => {
				if (this.currentPickerToken === currentPickerToken) {
					this.pickOpenWidget.hide(HideReason.CANCELED);
				}
			});
282

E
Erich Gamma 已提交
283 284 285
			let picksPromiseDone = false;

			// Resolve picks
286
			picksPromise.then(picks => {
J
Johannes Rieken 已提交
287
				if (this.currentPickerToken !== currentPickerToken) {
288
					return complete(void 0); // Return as canceled if another request came after or user canceled
E
Erich Gamma 已提交
289 290 291 292 293
				}

				picksPromiseDone = true;

				// Reset Progress
294
				this.pickOpenWidget.getProgressBar().stop().hide();
E
Erich Gamma 已提交
295 296

				// Model
297
				const model = new QuickOpenModel([], new PickOpenActionProvider());
298
				const entries = picks.map((e, index) => this.instantiationService.createInstance(PickOpenEntry, e, index, () => progress(e), () => this.pickOpenWidget.refresh()));
E
Erich Gamma 已提交
299
				if (picks.length === 0) {
300
					entries.push(this.instantiationService.createInstance(PickOpenEntry, { label: nls.localize('emptyPicks', "There are no entries to pick from") }, 0, null, null));
E
Erich Gamma 已提交
301 302 303 304 305
				}

				model.setEntries(entries);

				// Handlers
306
				const callbacks = {
E
Erich Gamma 已提交
307 308
					onOk: () => {
						if (picks.length === 0) {
309
							return complete(null);
E
Erich Gamma 已提交
310 311 312
						}

						let index = -1;
313
						let context: IEntryRunContext;
B
Benjamin Pasero 已提交
314
						entries.forEach(entry => {
315
							if (entry.shouldRunWithContext) {
B
Benjamin Pasero 已提交
316
								index = entry.index;
317
								context = entry.shouldRunWithContext;
E
Erich Gamma 已提交
318 319 320
							}
						});

321 322
						const selectedPick = picks[index];

B
Benjamin Pasero 已提交
323
						if (selectedPick && typeof selectedPick.run === 'function') {
324 325 326 327
							selectedPick.run(context);
						}

						complete(selectedPick || null);
E
Erich Gamma 已提交
328 329
					},
					onCancel: () => complete(void 0),
330
					onFocusLost: () => !this.closeOnFocusLost || options.ignoreFocusLost,
E
Erich Gamma 已提交
331 332
					onType: (value: string) => {

333
						// the caller takes care of all input
J
wip  
Johannes Rieken 已提交
334 335
						if (options.onDidType) {
							options.onDidType(value);
336
							return;
J
wip  
Johannes Rieken 已提交
337 338
						}

E
Erich Gamma 已提交
339 340 341 342 343 344 345 346
						if (picks.length === 0) {
							return;
						}

						value = value ? strings.trim(value) : value;

						// Reset filtering
						if (!value) {
347
							entries.forEach(e => {
E
Erich Gamma 已提交
348 349 350 351 352
								e.setHighlights(null);
								e.setHidden(false);
							});
						}

353
						// Filter by value (since we support octicons, use octicon aware fuzzy matching)
E
Erich Gamma 已提交
354
						else {
355
							entries.forEach(entry => {
356
								const { labelHighlights, descriptionHighlights, detailHighlights } = entry.matchesFuzzy(value, options);
E
Erich Gamma 已提交
357

358
								if (entry.shouldAlwaysShow() || labelHighlights || descriptionHighlights || detailHighlights) {
359
									entry.setHighlights(labelHighlights, descriptionHighlights, detailHighlights);
E
Erich Gamma 已提交
360 361
									entry.setHidden(false);
								} else {
362
									entry.setHighlights(null, null, null);
E
Erich Gamma 已提交
363 364 365 366 367
									entry.setHidden(true);
								}
							});
						}

368
						// Sort by value
B
Benjamin Pasero 已提交
369
						const normalizedSearchValue = value ? strings.stripWildcards(value.toLowerCase()) : value;
370 371 372 373 374
						model.entries.sort((pickA: PickOpenEntry, pickB: PickOpenEntry) => {
							if (!value) {
								return pickA.index - pickB.index; // restore natural order
							}

375
							return compareEntries(pickA, pickB, normalizedSearchValue);
376 377
						});

E
Erich Gamma 已提交
378 379
						this.pickOpenWidget.refresh(model, value ? { autoFocusFirstEntry: true } : autoFocus);
					},
B
Benjamin Pasero 已提交
380
					onShow: () => this.handleOnShow(true),
B
Benjamin Pasero 已提交
381
					onHide: (reason: HideReason) => this.handleOnHide(true, reason)
382 383
				};
				this.pickOpenWidget.setCallbacks(callbacks);
E
Erich Gamma 已提交
384 385 386

				// Set input
				if (!this.pickOpenWidget.isVisible()) {
B
Benjamin Pasero 已提交
387
					this.pickOpenWidget.show(model, { autoFocus, quickNavigateConfiguration: options.quickNavigateConfiguration });
E
Erich Gamma 已提交
388 389 390
				} else {
					this.pickOpenWidget.setInput(model, autoFocus);
				}
391 392 393 394 395 396 397

				// The user might have typed something (or options.value was set) so we need to play back
				// the input box value through our callbacks to filter the result accordingly.
				const inputValue = this.pickOpenWidget.getInputBox().value;
				if (inputValue) {
					callbacks.onType(inputValue);
				}
398 399 400 401 402
			}, (err) => {
				this.pickOpenWidget.hide();

				error(err);
			});
E
Erich Gamma 已提交
403 404

			// Progress if task takes a long time
405
			TPromise.timeout(800).then(() => {
E
Erich Gamma 已提交
406
				if (!picksPromiseDone && this.currentPickerToken === currentPickerToken) {
407
					this.pickOpenWidget.getProgressBar().infinite().show();
E
Erich Gamma 已提交
408 409 410 411 412 413 414 415 416 417
				}
			});

			// Show picker empty if resolving takes a while
			if (!picksPromiseDone) {
				this.pickOpenWidget.show(new QuickOpenModel());
			}
		});
	}

418 419 420 421 422 423 424
	public accept(): void {
		[this.quickOpenWidget, this.pickOpenWidget].forEach(w => {
			if (w && w.isVisible()) {
				w.accept();
			}
		});
	}
425

426 427 428 429 430 431 432 433 434 435 436 437 438 439
	public focus(): void {
		[this.quickOpenWidget, this.pickOpenWidget].forEach(w => {
			if (w && w.isVisible()) {
				w.focus();
			}
		});
	}

	public close(): void {
		[this.quickOpenWidget, this.pickOpenWidget].forEach(w => {
			if (w && w.isVisible()) {
				w.hide(HideReason.CANCELED);
			}
		});
440 441
	}

442
	private emitQuickOpenVisibilityChange(isVisible: boolean): void {
443 444 445 446
		if (isVisible) {
			this._onShow.fire();
		} else {
			this._onHide.fire();
447 448 449
		}
	}

B
Benjamin Pasero 已提交
450 451
	public show(prefix?: string, options?: IShowOptions): TPromise<void> {
		let quickNavigateConfiguration = options ? options.quickNavigateConfiguration : void 0;
452
		let inputSelection = options ? options.inputSelection : void 0;
B
Benjamin Pasero 已提交
453
		let autoFocus = options ? options.autoFocus : void 0;
B
Benjamin Pasero 已提交
454

455
		const promiseCompletedOnHide = new TPromise<void>(c => {
E
Erich Gamma 已提交
456 457 458 459
			this.promisesToCompleteOnHide.push(c);
		});

		// Telemetry: log that quick open is shown and log the mode
460 461
		const registry = Registry.as<IQuickOpenRegistry>(Extensions.Quickopen);
		const handlerDescriptor = registry.getQuickOpenHandler(prefix) || registry.getDefaultQuickOpenHandler();
E
Erich Gamma 已提交
462

463
		// Trigger onOpen
B
Benjamin Pasero 已提交
464
		this.resolveHandler(handlerDescriptor).done(null, errors.onUnexpectedError);
465

E
Erich Gamma 已提交
466 467 468
		// Create upon first open
		if (!this.quickOpenWidget) {
			this.quickOpenWidget = new QuickOpenWidget(
469
				document.getElementById(this.partService.getWorkbenchElementId()),
E
Erich Gamma 已提交
470
				{
471 472
					onOk: () => { /* ignore */ },
					onCancel: () => { /* ignore */ },
E
Erich Gamma 已提交
473
					onType: (value: string) => this.onType(value || ''),
B
Benjamin Pasero 已提交
474
					onShow: () => this.handleOnShow(false),
475 476
					onHide: (reason) => this.handleOnHide(false, reason),
					onFocusLost: () => !this.closeOnFocusLost
E
Erich Gamma 已提交
477
				}, {
478
					inputPlaceHolder: this.hasHandler(HELP_PREFIX) ? nls.localize('quickOpenInput', "Type '?' to get help on the actions you can take from here") : '',
J
Joao Moreno 已提交
479
					keyboardSupport: false,
480
					treeCreator: (container, config, opts) => this.instantiationService.createInstance(WorkbenchTree, container, config, opts)
481
				}
E
Erich Gamma 已提交
482
			);
483
			this.toUnbind.push(attachQuickOpenStyler(this.quickOpenWidget, this.themeService, { background: SIDE_BAR_BACKGROUND, foreground: SIDE_BAR_FOREGROUND }));
E
Erich Gamma 已提交
484

B
Benjamin Pasero 已提交
485
			const quickOpenContainer = this.quickOpenWidget.create();
486
			addClass(quickOpenContainer, 'show-file-icons');
487
			this.positionQuickOpenWidget();
E
Erich Gamma 已提交
488 489 490 491 492 493 494 495 496 497
		}

		// Layout
		if (this.layoutDimensions) {
			this.quickOpenWidget.layout(this.layoutDimensions);
		}

		// Show quick open with prefix or editor history
		if (!this.quickOpenWidget.isVisible() || quickNavigateConfiguration) {
			if (prefix) {
B
Benjamin Pasero 已提交
498
				this.quickOpenWidget.show(prefix, { quickNavigateConfiguration, inputSelection, autoFocus });
E
Erich Gamma 已提交
499
			} else {
500
				const editorHistory = this.getEditorHistoryWithGroupLabel();
E
Erich Gamma 已提交
501 502 503 504
				if (editorHistory.getEntries().length < 2) {
					quickNavigateConfiguration = null; // If no entries can be shown, default to normal quick open mode
				}

B
Benjamin Pasero 已提交
505 506 507 508 509
				// Compute auto focus
				if (!autoFocus) {
					if (!quickNavigateConfiguration) {
						autoFocus = { autoFocusFirstEntry: true };
					} else {
510
						const visibleEditorCount = this.editorService.visibleEditors.length;
B
Benjamin Pasero 已提交
511 512
						autoFocus = { autoFocusFirstEntry: visibleEditorCount === 0, autoFocusSecondEntry: visibleEditorCount !== 0 };
					}
E
Erich Gamma 已提交
513 514
				}

515 516 517 518
				// Update context
				const registry = Registry.as<IQuickOpenRegistry>(Extensions.Quickopen);
				this.setQuickOpenContextKey(registry.getDefaultQuickOpenHandler().contextKey);

519
				this.quickOpenWidget.show(editorHistory, { quickNavigateConfiguration, autoFocus, inputSelection });
E
Erich Gamma 已提交
520 521 522 523 524
			}
		}

		// Otherwise reset the widget to the prefix that is passed in
		else {
525
			this.quickOpenWidget.show(prefix || '', { inputSelection });
E
Erich Gamma 已提交
526 527 528 529 530
		}

		return promiseCompletedOnHide;
	}

531
	private positionQuickOpenWidget(): void {
B
Benjamin Pasero 已提交
532
		const titlebarOffset = this.partService.getTitleBarOffset();
533 534

		if (this.quickOpenWidget) {
535
			this.quickOpenWidget.getElement().style.top = `${titlebarOffset}px`;
536 537 538
		}

		if (this.pickOpenWidget) {
539
			this.pickOpenWidget.getElement().style.top = `${titlebarOffset}px`;
540 541 542
		}
	}

B
Benjamin Pasero 已提交
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
	private handleOnShow(isPicker: boolean): void {
		if (isPicker && this.quickOpenWidget) {
			this.quickOpenWidget.hide(HideReason.FOCUS_LOST);
		} else if (!isPicker && this.pickOpenWidget) {
			this.pickOpenWidget.hide(HideReason.FOCUS_LOST);
		}

		this.emitQuickOpenVisibilityChange(true);
	}

	private handleOnHide(isPicker: boolean, reason: HideReason): void {
		if (!isPicker) {

			// Clear state
			this.previousActiveHandlerDescriptor = null;

			// Pass to handlers
			for (let prefix in this.mapResolvedHandlersToPrefix) {
				if (this.mapResolvedHandlersToPrefix.hasOwnProperty(prefix)) {
562
					const promise = this.mapResolvedHandlersToPrefix[prefix];
563 564
					promise.then(handler => {
						this.handlerOnOpenCalled[prefix] = false;
565 566

						handler.onClose(reason === HideReason.CANCELED); // Don't check if onOpen was called to preserve old behaviour for now
567
					});
B
Benjamin Pasero 已提交
568 569 570 571 572 573 574 575 576 577
				}
			}

			// Complete promises that are waiting
			while (this.promisesToCompleteOnHide.length) {
				this.promisesToCompleteOnHide.pop()(true);
			}
		}

		if (reason !== HideReason.FOCUS_LOST) {
B
Benjamin Pasero 已提交
578
			this.editorGroupService.activeGroup.focus(); // focus back to editor group unless user clicked somewhere else
B
Benjamin Pasero 已提交
579 580
		}

581 582 583 584
		// Reset context keys
		this.resetQuickOpenContextKeys();

		// Events
B
Benjamin Pasero 已提交
585 586 587
		this.emitQuickOpenVisibilityChange(false);
	}

588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
	private resetQuickOpenContextKeys(): void {
		Object.keys(this.mapContextKeyToContext).forEach(k => this.mapContextKeyToContext[k].reset());
	}

	private setQuickOpenContextKey(id?: string): void {
		let key: IContextKey<boolean>;
		if (id) {
			key = this.mapContextKeyToContext[id];
			if (!key) {
				key = new RawContextKey<boolean>(id, false).bindTo(this.contextKeyService);
				this.mapContextKeyToContext[id] = key;
			}
		}

		if (key && key.get()) {
			return; // already active context
		}

		this.resetQuickOpenContextKeys();

		if (key) {
			key.set(true);
		}
	}

E
Erich Gamma 已提交
613
	private hasHandler(prefix: string): boolean {
614
		return !!Registry.as<IQuickOpenRegistry>(Extensions.Quickopen).getQuickOpenHandler(prefix);
E
Erich Gamma 已提交
615 616
	}

617
	private getEditorHistoryWithGroupLabel(): QuickOpenModel {
618
		const entries: QuickOpenEntry[] = this.editorHistoryHandler.getResults();
E
Erich Gamma 已提交
619 620 621

		// Apply label to first entry
		if (entries.length > 0) {
622
			entries[0] = new EditorHistoryEntryGroup(entries[0], nls.localize('historyMatches', "recently opened"), false);
E
Erich Gamma 已提交
623 624 625 626 627 628 629
		}

		return new QuickOpenModel(entries, this.actionProvider);
	}

	private onType(value: string): void {

630 631 632
		// look for a handler
		const registry = Registry.as<IQuickOpenRegistry>(Extensions.Quickopen);
		const handlerDescriptor = registry.getQuickOpenHandler(value);
633
		const defaultHandlerDescriptor = registry.getDefaultQuickOpenHandler();
634
		const instantProgress = handlerDescriptor && handlerDescriptor.instantProgress;
635
		const contextKey = handlerDescriptor ? handlerDescriptor.contextKey : defaultHandlerDescriptor.contextKey;
636

E
Erich Gamma 已提交
637
		// Use a generated token to avoid race conditions from long running promises
638
		const currentResultToken = defaultGenerator.nextId();
E
Erich Gamma 已提交
639 640
		this.currentResultToken = currentResultToken;

641 642
		// Reset Progress
		if (!instantProgress) {
643
			this.quickOpenWidget.getProgressBar().stop().hide();
644 645
		}

E
Erich Gamma 已提交
646 647 648
		// Reset Extra Class
		this.quickOpenWidget.setExtraClass(null);

649 650 651
		// Update context
		this.setQuickOpenContextKey(contextKey);

E
Erich Gamma 已提交
652
		// Remove leading and trailing whitespace
653
		const trimmedValue = strings.trim(value);
E
Erich Gamma 已提交
654 655 656

		// If no value provided, default to editor history
		if (!trimmedValue) {
657 658 659 660 661

			// Trigger onOpen
			this.resolveHandler(handlerDescriptor || defaultHandlerDescriptor)
				.done(null, errors.onUnexpectedError);

662
			this.quickOpenWidget.setInput(this.getEditorHistoryWithGroupLabel(), { autoFocusFirstEntry: true });
663

E
Erich Gamma 已提交
664 665 666 667 668 669 670 671 672 673 674 675
			return;
		}

		let resultPromise: TPromise<void>;
		let resultPromiseDone = false;

		if (handlerDescriptor) {
			resultPromise = this.handleSpecificHandler(handlerDescriptor, value, currentResultToken);
		}

		// Otherwise handle default handlers if no specific handler present
		else {
676
			resultPromise = this.handleDefaultHandler(defaultHandlerDescriptor, value, currentResultToken);
E
Erich Gamma 已提交
677 678 679 680 681 682
		}

		// Remember as the active one
		this.previousActiveHandlerDescriptor = handlerDescriptor;

		// Progress if task takes a long time
683
		TPromise.timeout(instantProgress ? 0 : 800).then(() => {
E
Erich Gamma 已提交
684
			if (!resultPromiseDone && currentResultToken === this.currentResultToken) {
685
				this.quickOpenWidget.getProgressBar().infinite().show();
E
Erich Gamma 已提交
686 687 688 689 690 691 692 693
			}
		});

		// Promise done handling
		resultPromise.done(() => {
			resultPromiseDone = true;

			if (currentResultToken === this.currentResultToken) {
694
				this.quickOpenWidget.getProgressBar().hide();
E
Erich Gamma 已提交
695 696 697 698
			}
		}, (error: any) => {
			resultPromiseDone = true;
			errors.onUnexpectedError(error);
699
			this.notificationService.error(types.isString(error) ? new Error(error) : error);
E
Erich Gamma 已提交
700 701 702
		});
	}

703
	private handleDefaultHandler(handler: QuickOpenHandlerDescriptor, value: string, currentResultToken: string): TPromise<void> {
704

E
Erich Gamma 已提交
705
		// Fill in history results if matching
706
		const matchingHistoryEntries = this.editorHistoryHandler.getResults(value);
E
Erich Gamma 已提交
707
		if (matchingHistoryEntries.length > 0) {
708
			matchingHistoryEntries[0] = new EditorHistoryEntryGroup(matchingHistoryEntries[0], nls.localize('historyMatches', "recently opened"), false);
E
Erich Gamma 已提交
709 710
		}

711 712 713
		// Resolve
		return this.resolveHandler(handler).then(resolvedHandler => {
			const quickOpenModel = new QuickOpenModel(matchingHistoryEntries, this.actionProvider);
E
Erich Gamma 已提交
714

715
			let inputSet = false;
E
Erich Gamma 已提交
716

717 718
			// If we have matching entries from history we want to show them directly and not wait for the other results to come in
			// This also applies when we used to have entries from a previous run and now there are no more history results matching
719 720 721 722
			const previousInput = this.quickOpenWidget.getInput();
			const wasShowingHistory = previousInput && previousInput.entries && previousInput.entries.some(e => e instanceof EditorHistoryEntry || e instanceof EditorHistoryEntryGroup);
			if (wasShowingHistory || matchingHistoryEntries.length > 0) {
				(resolvedHandler.hasShortResponseTime() ? TPromise.timeout(QuickOpenController.MAX_SHORT_RESPONSE_TIME) : TPromise.as(undefined)).then(() => {
723 724 725 726 727 728 729
					if (this.currentResultToken === currentResultToken && !inputSet) {
						this.quickOpenWidget.setInput(quickOpenModel, { autoFocusFirstEntry: true });
						inputSet = true;
					}
				});
			}

730
			// Get results
731
			return resolvedHandler.getResults(value).then(result => {
732 733 734 735 736 737 738 739 740 741
				if (this.currentResultToken === currentResultToken) {

					// now is the time to show the input if we did not have set it before
					if (!inputSet) {
						this.quickOpenWidget.setInput(quickOpenModel, { autoFocusFirstEntry: true });
						inputSet = true;
					}

					// merge history and default handler results
					const handlerResults = (result && result.entries) || [];
742
					this.mergeResults(quickOpenModel, handlerResults, resolvedHandler.getGroupLabel());
743
				}
744
			});
E
Erich Gamma 已提交
745 746 747
		});
	}

748
	private mergeResults(quickOpenModel: QuickOpenModel, handlerResults: QuickOpenEntry[], groupLabel: string): void {
E
Erich Gamma 已提交
749 750

		// Remove results already showing by checking for a "resource" property
751 752
		const mapEntryToResource = this.mapEntriesToResource(quickOpenModel);
		const additionalHandlerResults: QuickOpenEntry[] = [];
E
Erich Gamma 已提交
753
		for (let i = 0; i < handlerResults.length; i++) {
754 755
			const result = handlerResults[i];
			const resource = result.getResource();
E
Erich Gamma 已提交
756

757
			if (!result.mergeWithEditorHistory() || !resource || !mapEntryToResource[resource.toString()]) {
E
Erich Gamma 已提交
758 759 760 761 762 763
				additionalHandlerResults.push(result);
			}
		}

		// Show additional handler results below any existing results
		if (additionalHandlerResults.length > 0) {
764
			const autoFocusFirstEntry = (quickOpenModel.getEntries().length === 0); // the user might have selected another entry meanwhile in local history (see https://github.com/Microsoft/vscode/issues/20828)
765 766
			const useTopBorder = quickOpenModel.getEntries().length > 0;
			additionalHandlerResults[0] = new QuickOpenEntryGroup(additionalHandlerResults[0], groupLabel, useTopBorder);
E
Erich Gamma 已提交
767
			quickOpenModel.addEntries(additionalHandlerResults);
768
			this.quickOpenWidget.refresh(quickOpenModel, { autoFocusFirstEntry });
E
Erich Gamma 已提交
769 770 771
		}

		// Otherwise if no results are present (even from histoy) indicate this to the user
772
		else if (quickOpenModel.getEntries().length === 0) {
E
Erich Gamma 已提交
773 774 775 776 777 778 779 780 781 782 783 784
			quickOpenModel.addEntries([new PlaceholderQuickOpenEntry(nls.localize('noResultsFound1', "No results found"))]);
			this.quickOpenWidget.refresh(quickOpenModel, { autoFocusFirstEntry: true });
		}
	}

	private handleSpecificHandler(handlerDescriptor: QuickOpenHandlerDescriptor, value: string, currentResultToken: string): TPromise<void> {
		return this.resolveHandler(handlerDescriptor).then((resolvedHandler: QuickOpenHandler) => {

			// Remove handler prefix from search value
			value = value.substr(handlerDescriptor.prefix.length);

			// Return early if the handler can not run in the current environment and inform the user
785
			const canRun = resolvedHandler.canRun();
E
Erich Gamma 已提交
786
			if (types.isUndefinedOrNull(canRun) || (typeof canRun === 'boolean' && !canRun) || typeof canRun === 'string') {
787
				const placeHolderLabel = (typeof canRun === 'string') ? canRun : nls.localize('canNotRunPlaceholder', "This quick open handler can not be used in the current context");
E
Erich Gamma 已提交
788 789

				const model = new QuickOpenModel([new PlaceholderQuickOpenEntry(placeHolderLabel)], this.actionProvider);
790
				this.showModel(model, resolvedHandler.getAutoFocus(value, { model, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), resolvedHandler.getAriaLabel());
E
Erich Gamma 已提交
791 792 793 794 795

				return TPromise.as(null);
			}

			// Support extra class from handler
796
			const extraClass = resolvedHandler.getClass();
E
Erich Gamma 已提交
797 798 799 800 801 802 803 804 805 806
			if (extraClass) {
				this.quickOpenWidget.setExtraClass(extraClass);
			}

			// When handlers change, clear the result list first before loading the new results
			if (this.previousActiveHandlerDescriptor !== handlerDescriptor) {
				this.clearModel();
			}

			// Receive Results from Handler and apply
807
			return resolvedHandler.getResults(value).then(result => {
E
Erich Gamma 已提交
808 809 810
				if (this.currentResultToken === currentResultToken) {
					if (!result || !result.entries.length) {
						const model = new QuickOpenModel([new PlaceholderQuickOpenEntry(resolvedHandler.getEmptyLabel(value))]);
811
						this.showModel(model, resolvedHandler.getAutoFocus(value, { model, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), resolvedHandler.getAriaLabel());
E
Erich Gamma 已提交
812
					} else {
813
						this.showModel(result, resolvedHandler.getAutoFocus(value, { model: result, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), resolvedHandler.getAriaLabel());
E
Erich Gamma 已提交
814 815 816 817 818 819
					}
				}
			});
		});
	}

820
	private showModel(model: IModel<any>, autoFocus?: IAutoFocus, ariaLabel?: string): void {
E
Erich Gamma 已提交
821 822 823 824 825 826 827 828 829

		// If the given model is already set in the widget, refresh and return early
		if (this.quickOpenWidget.getInput() === model) {
			this.quickOpenWidget.refresh(model, autoFocus);

			return;
		}

		// Otherwise just set it
830
		this.quickOpenWidget.setInput(model, autoFocus, ariaLabel);
E
Erich Gamma 已提交
831 832 833 834 835 836 837
	}

	private clearModel(): void {
		this.showModel(new QuickOpenModel(), null);
	}

	private mapEntriesToResource(model: QuickOpenModel): { [resource: string]: QuickOpenEntry; } {
838 839
		const entries = model.getEntries();
		const mapEntryToPath: { [path: string]: QuickOpenEntry; } = {};
E
Erich Gamma 已提交
840 841 842 843 844 845 846 847 848 849
		entries.forEach((entry: QuickOpenEntry) => {
			if (entry.getResource()) {
				mapEntryToPath[entry.getResource().toString()] = entry;
			}
		});

		return mapEntryToPath;
	}

	private resolveHandler(handler: QuickOpenHandlerDescriptor): TPromise<QuickOpenHandler> {
850
		let result = this._resolveHandler(handler);
851

852 853 854 855 856 857 858
		const id = handler.getId();
		if (!this.handlerOnOpenCalled[id]) {
			const original = result;
			this.handlerOnOpenCalled[id] = true;
			result = this.mapResolvedHandlersToPrefix[id] = original.then(resolved => {
				this.mapResolvedHandlersToPrefix[id] = original;
				resolved.onOpen();
859

860 861 862
				return resolved;
			});
		}
863

864
		return result.then<QuickOpenHandler>(null, (error) => {
865
			delete this.mapResolvedHandlersToPrefix[id];
866

867
			return TPromise.wrapError(new Error(`Unable to instantiate quick open handler ${handler.getId()}: ${JSON.stringify(error)}`));
868 869 870 871
		});
	}

	private _resolveHandler(handler: QuickOpenHandlerDescriptor): TPromise<QuickOpenHandler> {
872
		const id = handler.getId();
E
Erich Gamma 已提交
873 874 875

		// Return Cached
		if (this.mapResolvedHandlersToPrefix[id]) {
876
			return this.mapResolvedHandlersToPrefix[id];
E
Erich Gamma 已提交
877 878 879
		}

		// Otherwise load and create
880
		return this.mapResolvedHandlersToPrefix[id] = TPromise.as(handler.instantiate(this.instantiationService));
E
Erich Gamma 已提交
881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906
	}

	public layout(dimension: Dimension): void {
		this.layoutDimensions = dimension;
		if (this.quickOpenWidget) {
			this.quickOpenWidget.layout(this.layoutDimensions);
		}

		if (this.pickOpenWidget) {
			this.pickOpenWidget.layout(this.layoutDimensions);
		}
	}

	public dispose(): void {
		if (this.quickOpenWidget) {
			this.quickOpenWidget.dispose();
		}

		if (this.pickOpenWidget) {
			this.pickOpenWidget.dispose();
		}

		super.dispose();
	}
}

907
class PlaceholderQuickOpenEntry extends QuickOpenEntryGroup {
E
Erich Gamma 已提交
908 909 910 911 912 913 914 915 916 917 918 919 920
	private placeHolderLabel: string;

	constructor(placeHolderLabel: string) {
		super();

		this.placeHolderLabel = placeHolderLabel;
	}

	public getLabel(): string {
		return this.placeHolderLabel;
	}
}

921
class PickOpenEntry extends PlaceholderQuickOpenEntry implements IPickOpenItem {
922
	private _shouldRunWithContext: IEntryRunContext;
E
Erich Gamma 已提交
923
	private description: string;
J
Johannes Rieken 已提交
924
	private detail: string;
925
	private tooltip: string;
926
	private descriptionTooltip: string;
B
Benjamin Pasero 已提交
927 928 929
	private hasSeparator: boolean;
	private separatorLabel: string;
	private alwaysShow: boolean;
B
Benjamin Pasero 已提交
930
	private resource: URI;
931
	private fileKind: FileKind;
932
	private _action: IAction;
933
	private removed: boolean;
934
	private payload: any;
935 936 937
	private labelOcticons: IParsedOcticons;
	private descriptionOcticons: IParsedOcticons;
	private detailOcticons: IParsedOcticons;
E
Erich Gamma 已提交
938

939
	constructor(
B
Benjamin Pasero 已提交
940
		item: IPickOpenEntry,
941
		private _index: number,
B
Benjamin Pasero 已提交
942
		private onPreview: () => void,
943
		private onRemove: () => void,
944 945
		@IModeService private modeService: IModeService,
		@IModelService private modelService: IModelService
946
	) {
B
Benjamin Pasero 已提交
947
		super(item.label);
E
Erich Gamma 已提交
948

B
Benjamin Pasero 已提交
949 950
		this.description = item.description;
		this.detail = item.detail;
951
		this.tooltip = item.tooltip;
952 953
		this.descriptionOcticons = item.description ? parseOcticons(item.description) : void 0;
		this.descriptionTooltip = this.descriptionOcticons ? this.descriptionOcticons.text : void 0;
B
Benjamin Pasero 已提交
954 955 956
		this.hasSeparator = item.separator && item.separator.border;
		this.separatorLabel = item.separator && item.separator.label;
		this.alwaysShow = item.alwaysShow;
957
		this._action = item.action;
958
		this.payload = item.payload;
B
Benjamin Pasero 已提交
959 960 961

		const fileItem = <IFilePickOpenEntry>item;
		this.resource = fileItem.resource;
962
		this.fileKind = fileItem.fileKind;
B
Benjamin Pasero 已提交
963 964
	}

965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981
	public matchesFuzzy(query: string, options: IInternalPickOptions): { labelHighlights: IMatch[], descriptionHighlights: IMatch[], detailHighlights: IMatch[] } {
		if (!this.labelOcticons) {
			this.labelOcticons = parseOcticons(this.getLabel()); // parse on demand
		}

		const detail = this.getDetail();
		if (detail && options.matchOnDetail && !this.detailOcticons) {
			this.detailOcticons = parseOcticons(detail); // parse on demand
		}

		return {
			labelHighlights: matchesFuzzyOcticonAware(query, this.labelOcticons),
			descriptionHighlights: options.matchOnDescription && this.descriptionOcticons ? matchesFuzzyOcticonAware(query, this.descriptionOcticons) : void 0,
			detailHighlights: options.matchOnDetail && this.detailOcticons ? matchesFuzzyOcticonAware(query, this.detailOcticons) : void 0
		};
	}

982 983 984 985
	public getPayload(): any {
		return this.payload;
	}

986 987 988 989 990 991 992 993 994 995 996
	public remove(): void {
		super.setHidden(true);
		this.removed = true;

		this.onRemove();
	}

	public isHidden(): boolean {
		return this.removed || super.isHidden();
	}

997 998 999 1000
	public get action(): IAction {
		return this._action;
	}

1001 1002 1003 1004
	public get index(): number {
		return this._index;
	}

1005
	public getLabelOptions(): IIconLabelValueOptions {
B
Benjamin Pasero 已提交
1006
		return {
1007
			extraClasses: this.resource ? getIconClasses(this.modelService, this.modeService, this.resource, this.fileKind) : []
B
Benjamin Pasero 已提交
1008
		};
E
Erich Gamma 已提交
1009 1010
	}

1011 1012
	public get shouldRunWithContext(): IEntryRunContext {
		return this._shouldRunWithContext;
E
Erich Gamma 已提交
1013 1014 1015 1016 1017 1018
	}

	public getDescription(): string {
		return this.description;
	}

J
Johannes Rieken 已提交
1019 1020
	public getDetail(): string {
		return this.detail;
1021 1022
	}

1023 1024 1025 1026
	public getTooltip(): string {
		return this.tooltip;
	}

1027 1028 1029 1030
	public getDescriptionTooltip(): string {
		return this.descriptionTooltip;
	}

1031
	public showBorder(): boolean {
B
Benjamin Pasero 已提交
1032 1033 1034 1035 1036
		return this.hasSeparator;
	}

	public getGroupLabel(): string {
		return this.separatorLabel;
1037 1038
	}

1039 1040 1041 1042
	public shouldAlwaysShow(): boolean {
		return this.alwaysShow;
	}

1043 1044 1045 1046
	public getResource(): URI {
		return this.resource;
	}

1047
	public run(mode: Mode, context: IEntryRunContext): boolean {
E
Erich Gamma 已提交
1048
		if (mode === Mode.OPEN) {
1049
			this._shouldRunWithContext = context;
E
Erich Gamma 已提交
1050 1051 1052 1053 1054 1055 1056 1057

			return true;
		}

		if (mode === Mode.PREVIEW && this.onPreview) {
			this.onPreview();
		}

1058 1059 1060 1061
		return false;
	}
}

1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083
class PickOpenActionProvider implements IActionProvider {
	public hasActions(tree: ITree, element: PickOpenEntry): boolean {
		return !!element.action;
	}

	public getActions(tree: ITree, element: PickOpenEntry): TPromise<IAction[]> {
		return TPromise.as(element.action ? [element.action] : []);
	}

	public hasSecondaryActions(tree: ITree, element: PickOpenEntry): boolean {
		return false;
	}

	public getSecondaryActions(tree: ITree, element: PickOpenEntry): TPromise<IAction[]> {
		return TPromise.as([]);
	}

	public getActionItem(tree: ITree, element: PickOpenEntry, action: Action): BaseActionItem {
		return null;
	}
}

1084
class EditorHistoryHandler {
1085
	private scorerCache: ScorerCache;
1086 1087 1088 1089

	constructor(
		@IHistoryService private historyService: IHistoryService,
		@IInstantiationService private instantiationService: IInstantiationService,
1090
		@IFileService private fileService: IFileService
1091
	) {
1092
		this.scorerCache = Object.create(null);
1093 1094 1095
	}

	public getResults(searchValue?: string): QuickOpenEntry[] {
1096 1097

		// Massage search for scoring
1098
		const query = prepareQuery(searchValue);
1099 1100 1101

		// Just return all if we are not searching
		const history = this.historyService.getHistory();
1102
		if (!query.value) {
1103 1104 1105
			return history.map(input => this.instantiationService.createInstance(EditorHistoryEntry, input));
		}

1106 1107
		// Otherwise filter by search value and sort by score. Include matches on description
		// in case the user is explicitly including path separators.
1108
		const accessor = query.containsPathSeparator ? MatchOnDescription : DoNotMatchOnDescription;
1109
		return history
1110

1111 1112 1113 1114 1115 1116 1117 1118
			// For now, only support to match on inputs that provide resource information
			.filter(input => {
				let resource: URI;
				if (input instanceof EditorInput) {
					resource = resourceForEditorHistory(input, this.fileService);
				} else {
					resource = (input as IResourceInput).resource;
				}
1119

1120 1121
				return !!resource;
			})
1122

1123 1124
			// Conver to quick open entries
			.map(input => this.instantiationService.createInstance(EditorHistoryEntry, input))
1125

1126 1127
			// Make sure the search value is matching
			.filter(e => {
1128
				const itemScore = scoreItem(e, query, false, accessor, this.scorerCache);
1129 1130 1131
				if (!itemScore.score) {
					return false;
				}
1132

1133
				e.setHighlights(itemScore.labelMatch, itemScore.descriptionMatch);
1134

1135 1136
				return true;
			})
1137

B
Benjamin Pasero 已提交
1138 1139
			// Sort by score and provide a fallback sorter that keeps the
			// recency of items in case the score for items is the same
1140
			.sort((e1, e2) => compareItemsByScore(e1, e2, query, false, accessor, this.scorerCache, (e1, e2, query, accessor) => -1));
1141 1142 1143 1144
	}
}

class EditorHistoryItemAccessorClass extends QuickOpenItemAccessorClass {
1145

1146 1147 1148 1149 1150 1151
	constructor(private allowMatchOnDescription: boolean) {
		super();
	}

	public getItemDescription(entry: QuickOpenEntry): string {
		return this.allowMatchOnDescription ? entry.getDescription() : void 0;
1152 1153 1154
	}
}

1155 1156 1157
const MatchOnDescription = new EditorHistoryItemAccessorClass(true);
const DoNotMatchOnDescription = new EditorHistoryItemAccessorClass(false);

1158 1159 1160 1161
export class EditorHistoryEntryGroup extends QuickOpenEntryGroup {
	// Marker class
}

1162
export class EditorHistoryEntry extends EditorQuickOpenEntry {
1163
	private input: IEditorInput | IResourceInput;
1164
	private resource: URI;
1165 1166
	private label: string;
	private description: string;
1167
	private dirty: boolean;
1168 1169

	constructor(
1170
		input: IEditorInput | IResourceInput,
1171
		@INextEditorService editorService: INextEditorService,
B
Benjamin Pasero 已提交
1172
		@IModeService private modeService: IModeService,
1173
		@IModelService private modelService: IModelService,
1174 1175
		@ITextFileService private textFileService: ITextFileService,
		@IWorkspaceContextService contextService: IWorkspaceContextService,
1176
		@IConfigurationService private configurationService: IConfigurationService,
1177
		@IEnvironmentService environmentService: IEnvironmentService,
B
Benjamin Pasero 已提交
1178
		@IFileService fileService: IFileService
1179 1180 1181 1182
	) {
		super(editorService);

		this.input = input;
1183 1184

		if (input instanceof EditorInput) {
1185
			this.resource = resourceForEditorHistory(input, fileService);
1186 1187
			this.label = input.getName();
			this.description = input.getDescription();
1188
			this.dirty = input.isDirty();
1189 1190 1191
		} else {
			const resourceInput = input as IResourceInput;
			this.resource = resourceInput.resource;
1192
			this.label = labels.getBaseLabel(resourceInput.resource);
B
Benjamin Pasero 已提交
1193
			this.description = labels.getPathLabel(resources.dirname(this.resource), contextService, environmentService);
1194 1195 1196 1197 1198
			this.dirty = this.resource && this.textFileService.isDirty(this.resource);

			if (this.dirty && this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
				this.dirty = false; // no dirty decoration if auto save is on with a short timeout
			}
1199
		}
1200 1201 1202
	}

	public getIcon(): string {
1203
		return this.dirty ? 'dirty' : '';
1204 1205 1206
	}

	public getLabel(): string {
1207
		return this.label;
1208 1209
	}

1210
	public getLabelOptions(): IIconLabelValueOptions {
B
Benjamin Pasero 已提交
1211
		return {
1212
			extraClasses: getIconClasses(this.modelService, this.modeService, this.resource)
B
Benjamin Pasero 已提交
1213 1214 1215
		};
	}

1216 1217 1218 1219 1220
	public getAriaLabel(): string {
		return nls.localize('entryAriaLabel', "{0}, recently opened", this.getLabel());
	}

	public getDescription(): string {
1221
		return this.description;
1222 1223 1224 1225 1226 1227
	}

	public getResource(): URI {
		return this.resource;
	}

1228
	public getInput(): IEditorInput | IResourceInput {
1229 1230 1231 1232 1233
		return this.input;
	}

	public run(mode: Mode, context: IEntryRunContext): boolean {
		if (mode === Mode.OPEN) {
I
isidor 已提交
1234
			const sideBySide = !context.quickNavigateConfiguration && (context.keymods.alt || context.keymods.ctrlCmd);
1235
			const pinned = !this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor.enablePreviewFromQuickOpen || context.keymods.alt;
1236

1237
			if (this.input instanceof EditorInput) {
1238
				this.editorService.openEditor(this.input, { pinned }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
1239
			} else {
1240
				this.editorService.openEditor({ resource: (this.input as IResourceInput).resource, options: { pinned } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
1241
			}
1242 1243 1244 1245

			return true;
		}

W
Will Prater 已提交
1246
		return super.run(mode, context);
E
Erich Gamma 已提交
1247
	}
1248 1249
}

1250
function resourceForEditorHistory(input: EditorInput, fileService: IFileService): URI {
B
Benjamin Pasero 已提交
1251
	const resource = input ? input.getResource() : void 0;
1252 1253 1254

	// For the editor history we only prefer resources that are either untitled or
	// can be handled by the file service which indicates they are editable resources.
1255
	if (resource && (fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled)) {
1256 1257 1258 1259 1260 1261
		return resource;
	}

	return void 0;
}

1262 1263
export class RemoveFromEditorHistoryAction extends Action {

M
Matt Bierner 已提交
1264 1265
	public static readonly ID = 'workbench.action.removeFromEditorHistory';
	public static readonly LABEL = nls.localize('removeFromEditorHistory', "Remove From History");
1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299

	constructor(
		id: string,
		label: string,
		@IQuickOpenService private quickOpenService: IQuickOpenService,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IHistoryService private historyService: IHistoryService
	) {
		super(id, label);
	}

	public run(): TPromise<any> {
		interface IHistoryPickEntry extends IFilePickOpenEntry {
			input: IEditorInput | IResourceInput;
		}

		const history = this.historyService.getHistory();
		const picks: IHistoryPickEntry[] = history.map(h => {
			const entry = this.instantiationService.createInstance(EditorHistoryEntry, h);

			return <IHistoryPickEntry>{
				input: h,
				resource: entry.getResource(),
				label: entry.getLabel(),
				description: entry.getDescription()
			};
		});

		return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickHistory', "Select an editor entry to remove from history"), autoFocus: { autoFocusFirstEntry: true }, matchOnDescription: true }).then(pick => {
			if (pick) {
				this.historyService.remove(pick.input);
			}
		});
	}
J
Johannes Rieken 已提交
1300
}