suggestWidget.ts 23.9 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!./suggest';
J
Johannes Rieken 已提交
9
import * as nls from 'vs/nls';
10
import * as strings from 'vs/base/common/strings';
A
Alex Dima 已提交
11
import {isPromiseCanceledError, onUnexpectedError} from 'vs/base/common/errors';
J
Joao Moreno 已提交
12
import Event, { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
13
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
14 15 16 17 18 19 20 21 22 23 24
import * as timer from 'vs/base/common/timer';
import {TPromise} from 'vs/base/common/winjs.base';
import {addClass, append, emmet as $, hide, removeClass, show, toggleClass} from 'vs/base/browser/dom';
import {HighlightedLabel} from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import {IDelegate, IFocusChangeEvent, IRenderer, ISelectionChangeEvent} from 'vs/base/browser/ui/list/list';
import {List} from 'vs/base/browser/ui/list/listWidget';
import {ScrollableElement} from 'vs/base/browser/ui/scrollbar/scrollableElementImpl';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IKeybindingContextKey, IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {EventType, IModeSupportChangedEvent} from 'vs/editor/common/editorCommon';
25
import {SuggestRegistry} from 'vs/editor/common/modes';
A
Alex Dima 已提交
26
import {ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition} from 'vs/editor/browser/editorBrowser';
27
import {CONTEXT_SUGGESTION_SUPPORTS_ACCEPT_ON_KEY} from '../common/suggest';
A
Alex Dima 已提交
28 29
import {CompletionItem, CompletionModel} from './completionModel';
import {ICancelEvent, ISuggestEvent, ITriggerEvent, SuggestModel} from './suggestModel';
30
import {alert} from 'vs/base/browser/ui/aria/aria';
31
import {DomNodeScrollable} from 'vs/base/browser/ui/scrollbar/domNodeScrollable';
E
Erich Gamma 已提交
32 33 34 35 36

interface ISuggestionTemplateData {
	root: HTMLElement;
	icon: HTMLElement;
	colorspan: HTMLElement;
A
Alex Dima 已提交
37
	highlightedLabel: HighlightedLabel;
E
Erich Gamma 已提交
38
	typeLabel: HTMLElement;
J
Joao Moreno 已提交
39
	documentationDetails: HTMLElement;
J
Joao Moreno 已提交
40
	documentation: HTMLElement;
E
Erich Gamma 已提交
41 42
}

J
Joao Moreno 已提交
43
class Renderer implements IRenderer<CompletionItem, ISuggestionTemplateData> {
E
Erich Gamma 已提交
44

J
Joao Moreno 已提交
45 46 47 48 49 50 51 52 53
	private triggerKeybindingLabel: string;

	constructor(
		private widget: SuggestWidget,
		@IKeybindingService keybindingService: IKeybindingService
	) {
		const keybindings = keybindingService.lookupKeybindings('editor.action.triggerSuggest');
		this.triggerKeybindingLabel = keybindings.length === 0 ? '' : ` (${keybindingService.getLabelFor(keybindings[0])})`;
	}
J
Joao Moreno 已提交
54

J
Joao Moreno 已提交
55 56 57
	get templateId(): string {
		return 'suggestion';
	}
E
Erich Gamma 已提交
58

J
Joao Moreno 已提交
59
	renderTemplate(container: HTMLElement): ISuggestionTemplateData {
J
Johannes Rieken 已提交
60
		const data = <ISuggestionTemplateData>Object.create(null);
E
Erich Gamma 已提交
61 62
		data.root = container;

J
Joao Moreno 已提交
63 64
		data.icon = append(container, $('.icon'));
		data.colorspan = append(data.icon, $('span.colorspan'));
E
Erich Gamma 已提交
65

J
Joao Moreno 已提交
66 67
		const text = append(container, $('.text'));
		const main = append(text, $('.main'));
A
Alex Dima 已提交
68
		data.highlightedLabel = new HighlightedLabel(main);
J
Joao Moreno 已提交
69
		data.typeLabel = append(main, $('span.type-label'));
J
Joao Moreno 已提交
70 71 72
		const docs = append(text, $('.docs'));
		data.documentation = append(docs, $('span.docs-text'));
		data.documentationDetails = append(docs, $('span.docs-details.octicon.octicon-info'));
J
Joao Moreno 已提交
73
		data.documentationDetails.title = nls.localize('readMore', "Read More...{0}", this.triggerKeybindingLabel);
E
Erich Gamma 已提交
74 75 76 77

		return data;
	}

J
Joao Moreno 已提交
78
	renderElement(element: CompletionItem, index: number, templateData: ISuggestionTemplateData): void {
J
Johannes Rieken 已提交
79 80
		const data = <ISuggestionTemplateData>templateData;
		const suggestion = (<CompletionItem>element).suggestion;
E
Erich Gamma 已提交
81

82 83 84 85 86 87
		if (suggestion.documentationLabel) {
			data.root.setAttribute('aria-label', nls.localize('suggestionWithDetailsAriaLabel', "{0}, suggestion, has details", suggestion.label));
		} else {
			data.root.setAttribute('aria-label', nls.localize('suggestionAriaLabel', "{0}, suggestion", suggestion.label));
		}

J
Johannes Rieken 已提交
88
		if (suggestion.type === 'customcolor') {
E
Erich Gamma 已提交
89
			data.icon.className = 'icon customcolor';
J
Johannes Rieken 已提交
90
			data.colorspan.style.backgroundColor = suggestion.label;
E
Erich Gamma 已提交
91 92 93 94 95
		} else {
			data.icon.className = 'icon ' + suggestion.type;
			data.colorspan.style.backgroundColor = '';
		}

J
Johannes Rieken 已提交
96
		data.highlightedLabel.set(suggestion.label, (<CompletionItem>element).highlights);
E
Erich Gamma 已提交
97
		data.typeLabel.textContent = suggestion.typeLabel || '';
J
Joao Moreno 已提交
98
		data.documentation.textContent = suggestion.documentationLabel || '';
J
Joao Moreno 已提交
99 100 101

		if (suggestion.documentationLabel) {
			show(data.documentationDetails);
J
Joao Moreno 已提交
102 103 104 105
			data.documentationDetails.onmousedown = e => {
				e.stopPropagation();
				e.preventDefault();
			};
J
Joao Moreno 已提交
106 107 108
			data.documentationDetails.onclick = e => {
				e.stopPropagation();
				e.preventDefault();
J
Joao Moreno 已提交
109
				this.widget.toggleDetails();
J
Joao Moreno 已提交
110 111 112
			};
		} else {
			hide(data.documentationDetails);
J
Joao Moreno 已提交
113
			data.documentationDetails.onmousedown = null;
J
Joao Moreno 已提交
114 115
			data.documentationDetails.onclick = null;
		}
E
Erich Gamma 已提交
116 117
	}

J
Joao Moreno 已提交
118 119 120 121
	disposeTemplate(templateData: ISuggestionTemplateData): void {
		templateData.highlightedLabel.dispose();
	}
}
E
Erich Gamma 已提交
122

J
Johannes Rieken 已提交
123
function computeScore(suggestion: string, currentWord: string, currentWordLowerCase: string): number {
J
Joao Moreno 已提交
124 125
	const suggestionLowerCase = suggestion.toLowerCase();
	let score = 0;
E
Erich Gamma 已提交
126

J
Joao Moreno 已提交
127
	for (let i = 0; i < currentWord.length && i < suggestion.length; i++) {
E
Erich Gamma 已提交
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
		if (currentWord[i] === suggestion[i]) {
			score += 2;
		} else if (currentWordLowerCase[i] === suggestionLowerCase[i]) {
			score += 1;
		} else {
			break;
		}
	}

	return score;
}

interface ITelemetryData {
	suggestionCount?: number;
	suggestedIndex?: number;
	selectedIndex?: number;
	hintLength?: number;
	wasCancelled?: boolean;
	wasAutomaticallyTriggered?: boolean;
}

J
Joao Moreno 已提交
149 150 151 152
enum State {
	Hidden,
	Loading,
	Empty,
J
Joao Moreno 已提交
153
	Open,
J
Joao Moreno 已提交
154 155 156 157 158 159 160
	Frozen,
	Details
}

class SuggestionDetails {

	private el: HTMLElement;
J
Joao Moreno 已提交
161
	private title: HTMLElement;
J
Joao Moreno 已提交
162
	private back: HTMLElement;
163 164
	private scrollable: DomNodeScrollable;
	private scrollbar: ScrollableElement;
165
	private body: HTMLElement;
J
Joao Moreno 已提交
166 167
	private type: HTMLElement;
	private docs: HTMLElement;
168
	private ariaLabel:string;
J
Joao Moreno 已提交
169

J
Joao Moreno 已提交
170
	constructor(container: HTMLElement, private widget: SuggestWidget) {
J
Joao Moreno 已提交
171
		this.el = append(container, $('.details'));
J
Joao Moreno 已提交
172 173
		const header = append(this.el, $('.header'));
		this.title = append(header, $('span.title'));
J
Joao Moreno 已提交
174
		this.back = append(header, $('span.go-back.octicon.octicon-mail-reply'));
J
Joao Moreno 已提交
175
		this.back.title = nls.localize('goback', "Go back");
176
		this.body = $('.body');
177 178 179
		this.scrollable = new DomNodeScrollable(this.body);
		this.scrollbar = new ScrollableElement(this.body, this.scrollable, {});
		append(this.el, this.scrollbar.getDomNode());
180 181
		this.type = append(this.body, $('p.type'));
		this.docs = append(this.body, $('p.docs'));
182 183

		this.ariaLabel = null;
J
Joao Moreno 已提交
184 185
	}

J
Joao Moreno 已提交
186 187 188
	get element() {
		return this.el;
	}
J
Joao Moreno 已提交
189 190 191

	render(item: CompletionItem): void {
		if (!item) {
J
Joao Moreno 已提交
192 193 194
			this.title.textContent = '';
			this.type.textContent = '';
			this.docs.textContent = '';
195
			this.ariaLabel = null;
J
Joao Moreno 已提交
196 197 198
			return;
		}

J
Joao Moreno 已提交
199
		this.title.innerText = item.suggestion.label;
J
Joao Moreno 已提交
200
		this.type.innerText = item.suggestion.typeLabel || '';
J
Joao Moreno 已提交
201
		this.docs.innerText = item.suggestion.documentationLabel;
J
Joao Moreno 已提交
202 203 204 205
		this.back.onmousedown = e => {
			e.preventDefault();
			e.stopPropagation();
		};
J
Joao Moreno 已提交
206 207 208
		this.back.onclick = e => {
			e.preventDefault();
			e.stopPropagation();
J
Joao Moreno 已提交
209
			this.widget.toggleDetails();
J
Joao Moreno 已提交
210
		};
211

212 213
		this.scrollbar.onElementDimensions();
		this.scrollable.onContentsDimensions();
214

215
		this.ariaLabel = strings.format('{0}\n{1}\n{2}', item.suggestion.label || '', item.suggestion.typeLabel || '', item.suggestion.documentationLabel || '');
216 217 218 219
	}

	getAriaLabel(): string {
		return this.ariaLabel;
J
Joao Moreno 已提交
220 221
	}

222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
	scrollDown(much = 8): void {
		this.body.scrollTop += much;
	}

	scrollUp(much = 8): void {
		this.body.scrollTop -= much;
	}

	pageDown(): void {
		this.scrollDown(80);
	}

	pageUp(): void {
		this.scrollUp(80);
	}

J
Joao Moreno 已提交
238
	dispose(): void {
239 240 241
		this.scrollbar.dispose();
		this.scrollable.dispose();

J
Joao Moreno 已提交
242 243 244
		this.el.parentElement.removeChild(this.el);
		this.el = null;
	}
J
Joao Moreno 已提交
245 246
}

247
export class SuggestWidget implements IContentWidget, IDelegate<CompletionItem>, IDisposable {
J
Joao Moreno 已提交
248

J
Johannes Rieken 已提交
249 250
	static ID: string = 'editor.widget.suggestWidget';
	static WIDTH: number = 438;
E
Erich Gamma 已提交
251

J
Johannes Rieken 已提交
252 253
	static LOADING_MESSAGE: string = nls.localize('suggestWidget.loading', "Loading...");
	static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions.");
E
Erich Gamma 已提交
254

255
	allowEditorOverflow: boolean = true; // Editor.IContentWidget.allowEditorOverflow
J
Joao Moreno 已提交
256

J
Joao Moreno 已提交
257
	private state: State;
E
Erich Gamma 已提交
258
	private isAuto: boolean;
J
Joao Moreno 已提交
259
	private shouldShowEmptySuggestionList: boolean;
E
Erich Gamma 已提交
260
	private suggestionSupportsAutoAccept: IKeybindingContextKey<boolean>;
261
	private loadingTimeout: number;
J
Joao Moreno 已提交
262
	private currentSuggestionDetails: TPromise<void>;
J
Joao Moreno 已提交
263
	private focusedItem: CompletionItem;
J
Joao Moreno 已提交
264
	private completionModel: CompletionModel;
J
Joao Moreno 已提交
265

E
Erich Gamma 已提交
266 267
	private telemetryData: ITelemetryData;
	private telemetryService: ITelemetryService;
A
Alex Dima 已提交
268
	private telemetryTimer: timer.ITimerEvent;
E
Erich Gamma 已提交
269 270

	private element: HTMLElement;
J
Joao Moreno 已提交
271
	private messageElement: HTMLElement;
J
Joao Moreno 已提交
272
	private listElement: HTMLElement;
J
Joao Moreno 已提交
273
	private details: SuggestionDetails;
J
Joao Moreno 已提交
274
	private list: List<CompletionItem>;
E
Erich Gamma 已提交
275

J
Joao Moreno 已提交
276 277
	private editorBlurTimeout: TPromise<void>;
	private showTimeout: TPromise<void>;
J
Joao Moreno 已提交
278
	private toDispose: IDisposable[];
J
Joao Moreno 已提交
279

J
Joao Moreno 已提交
280
	private _onDidVisibilityChange: Emitter<boolean> = new Emitter();
281
	get onDidVisibilityChange(): Event<boolean> { return this._onDidVisibilityChange.event; }
E
Erich Gamma 已提交
282

283
	constructor(
A
Alex Dima 已提交
284
		private editor: ICodeEditor,
285
		private model: SuggestModel,
J
Joao Moreno 已提交
286
		@IKeybindingService keybindingService: IKeybindingService,
J
Joao Moreno 已提交
287 288
		@ITelemetryService telemetryService: ITelemetryService,
		@IInstantiationService instantiationService: IInstantiationService
289
	) {
E
Erich Gamma 已提交
290
		this.isAuto = false;
J
Joao Moreno 已提交
291
		this.focusedItem = null;
J
Joao Moreno 已提交
292
		this.suggestionSupportsAutoAccept = keybindingService.createKey(CONTEXT_SUGGESTION_SUPPORTS_ACCEPT_ON_KEY, true);
E
Erich Gamma 已提交
293 294 295 296 297 298 299 300 301 302

		this.telemetryData = null;
		this.telemetryService = telemetryService;

		this.element = $('.editor-widget.suggest-widget.monaco-editor-background');
		this.element.style.width = SuggestWidget.WIDTH + 'px';
		this.element.style.top = '0';
		this.element.style.left = '0';

		if (!this.editor.getConfiguration().iconsInSuggestions) {
J
Joao Moreno 已提交
303
			addClass(this.element, 'no-icons');
E
Erich Gamma 已提交
304 305
		}

J
Joao Moreno 已提交
306
		this.messageElement = append(this.element, $('.message'));
J
Joao Moreno 已提交
307
		this.listElement = append(this.element, $('.tree'));
J
Joao Moreno 已提交
308
		this.details = new SuggestionDetails(this.element, this);
J
Joao Moreno 已提交
309 310

		let renderer: IRenderer<CompletionItem, any> = instantiationService.createInstance(Renderer, this);
J
Joao Moreno 已提交
311

312
		this.list = new List(this.listElement, this, [renderer]);
J
Joao Moreno 已提交
313 314

		this.toDispose = [
A
Alex Dima 已提交
315 316 317
			editor.addListener2(EventType.ModelChanged, () => this.onModelModeChanged()),
			editor.addListener2(EventType.ModelModeChanged, () => this.onModelModeChanged()),
			editor.addListener2(EventType.ModelModeSupportChanged, (e: IModeSupportChangedEvent) => e.suggestSupport && this.onModelModeChanged()),
J
Joao Moreno 已提交
318
			SuggestRegistry.onDidChange(() => this.onModelModeChanged()),
A
Alex Dima 已提交
319
			editor.addListener2(EventType.EditorTextBlur, () => this.onEditorBlur()),
J
Joao Moreno 已提交
320
			this.list.onSelectionChange(e => this.onListSelection(e)),
J
Joao Moreno 已提交
321
			this.list.onFocusChange(e => this.onListFocus(e)),
A
Alex Dima 已提交
322
			this.editor.addListener2(EventType.CursorSelectionChanged, () => this.onCursorSelectionChanged()),
J
Joao Moreno 已提交
323 324 325 326 327 328 329 330
			this.model.onDidTrigger(e => this.onDidTrigger(e)),
			this.model.onDidSuggest(e => this.onDidSuggest(e)),
			this.model.onDidCancel(e => this.onDidCancel(e))
		];

		this.onModelModeChanged();
		this.editor.addContentWidget(this);
		this.setState(State.Hidden);
331 332 333 334 335 336 337 338 339 340 341 342 343 344

		// TODO@Alex: this is useful, but spammy
		// var isVisible = false;
		// this.onDidVisibilityChange((newIsVisible) => {
		// 	if (isVisible === newIsVisible) {
		// 		return;
		// 	}
		// 	isVisible = newIsVisible;
		// 	if (isVisible) {
		// 		alert(nls.localize('suggestWidgetAriaVisible', "Suggestions opened"));
		// 	} else {
		// 		alert(nls.localize('suggestWidgetAriaInvisible', "Suggestions closed"));
		// 	}
		// });
J
Joao Moreno 已提交
345
	}
E
Erich Gamma 已提交
346

J
Joao Moreno 已提交
347 348 349 350
	private onCursorSelectionChanged(): void {
		if (this.state === State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
351

J
Joao Moreno 已提交
352 353
		this.editor.layoutContentWidget(this);
	}
J
Joao Moreno 已提交
354

J
Joao Moreno 已提交
355
	private onEditorBlur(): void {
J
Joao Moreno 已提交
356
		this.editorBlurTimeout = TPromise.timeout(150).then(() => {
J
Joao Moreno 已提交
357
			if (!this.editor.isFocused()) {
J
Joao Moreno 已提交
358 359 360 361 362
				this.setState(State.Hidden);
			}
		});
	}

J
Joao Moreno 已提交
363 364 365 366
	private onListSelection(e: ISelectionChangeEvent<CompletionItem>): void {
		if (!e.elements.length) {
			return;
		}
E
Erich Gamma 已提交
367

J
Joao Moreno 已提交
368 369 370 371 372 373 374 375 376 377 378 379 380 381
		this.telemetryData.selectedIndex = 0;
		this.telemetryData.wasCancelled = false;
		this.telemetryData.selectedIndex = e.indexes[0];
		this.submitTelemetryData();

		const item = e.elements[0];
		const container = item.container;
		const overwriteBefore = (typeof item.suggestion.overwriteBefore === 'undefined') ? container.currentWord.length : item.suggestion.overwriteBefore;
		const overwriteAfter = (typeof item.suggestion.overwriteAfter === 'undefined') ? 0 : Math.max(0, item.suggestion.overwriteAfter);
		this.model.accept(item.suggestion, overwriteBefore, overwriteAfter);

		alert(nls.localize('suggestionAriaAccepted', "{0}, accepted", item.suggestion.label));

		this.editor.focus();
J
Joao Moreno 已提交
382
	}
E
Erich Gamma 已提交
383

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
	private _getSuggestionAriaAlertLabel(item:CompletionItem): string {
		if (item.suggestion.documentationLabel) {
			return nls.localize('ariaCurrentSuggestionWithDetails',"{0}, suggestion, has details", item.suggestion.label);
		} else {
			return nls.localize('ariaCurrentSuggestion',"{0}, suggestion", item.suggestion.label);
		}
	}

	private _lastAriaAlertLabel: string;
	private _ariaAlert(newAriaAlertLabel:string): void {
		if (this._lastAriaAlertLabel === newAriaAlertLabel) {
			return;
		}
		this._lastAriaAlertLabel = newAriaAlertLabel;
		if (this._lastAriaAlertLabel) {
			alert(this._lastAriaAlertLabel);
		}
	}

J
Joao Moreno 已提交
403
	private onListFocus(e: IFocusChangeEvent<CompletionItem>): void {
J
Joao Moreno 已提交
404 405 406 407 408
		if (this.currentSuggestionDetails) {
			this.currentSuggestionDetails.cancel();
			this.currentSuggestionDetails = null;
		}

J
Joao Moreno 已提交
409
		if (!e.elements.length) {
410
			this._ariaAlert(null);
411 412 413 414

			// TODO@Alex: Chromium bug
			// this.editor.setAriaActiveDescendant(null);

J
Joao Moreno 已提交
415 416
			return;
		}
E
Erich Gamma 已提交
417

J
Joao Moreno 已提交
418
		const item = e.elements[0];
419
		this._ariaAlert(this._getSuggestionAriaAlertLabel(item));
420 421 422 423 424 425

		// TODO@Alex: Chromium bug
		// // TODO@Alex: the list is not done rendering...
		// setTimeout(() => {
		// 	this.editor.setAriaActiveDescendant(this.list.getElementId(e.indexes[0]));
		// }, 100);
E
Erich Gamma 已提交
426

J
Joao Moreno 已提交
427 428 429
		if (item === this.focusedItem) {
			return;
		}
E
Erich Gamma 已提交
430

J
Joao Moreno 已提交
431
		const index = e.indexes[0];
E
Erich Gamma 已提交
432

433
		this.suggestionSupportsAutoAccept.set(!item.suggestion.noAutoAccept);
J
Joao Moreno 已提交
434 435
		this.focusedItem = item;
		this.list.setFocus(index);
J
Joao Moreno 已提交
436
		this.updateWidgetHeight();
J
Joao Moreno 已提交
437
		this.list.reveal(index);
J
Joao Moreno 已提交
438

J
Joao Moreno 已提交
439 440 441 442 443 444 445 446 447
		const resource = this.editor.getModel().getAssociatedResource();
		const position = this.model.getRequestPosition() || this.editor.getPosition();

		this.currentSuggestionDetails = item.resolveDetails(resource, position)
			.then(details => {
				item.updateDetails(details);
				this.list.setFocus(index);
				this.updateWidgetHeight();
				this.list.reveal(index);
448 449

				this._ariaAlert(this._getSuggestionAriaAlertLabel(item));
J
Joao Moreno 已提交
450 451 452
			})
			.then(null, err => !isPromiseCanceledError(err) && onUnexpectedError(err))
			.then(() => this.currentSuggestionDetails = null);
J
Joao Moreno 已提交
453
	}
J
Joao Moreno 已提交
454

J
Joao Moreno 已提交
455 456 457 458
	private onModelModeChanged(): void {
		const model = this.editor.getModel();
		const supports = SuggestRegistry.all(model);
		this.shouldShowEmptySuggestionList = supports.some(s => s.shouldShowEmptySuggestionList());
J
Joao Moreno 已提交
459 460 461
	}

	private setState(state: State): void {
462
		const stateChanged = this.state !== state;
J
Joao Moreno 已提交
463 464
		this.state = state;

J
Joao Moreno 已提交
465 466
		toggleClass(this.element, 'frozen', state === State.Frozen);

J
Joao Moreno 已提交
467 468
		switch (state) {
			case State.Hidden:
J
Joao Moreno 已提交
469
				hide(this.messageElement, this.details.element);
J
Joao Moreno 已提交
470
				show(this.listElement);
J
Joao Moreno 已提交
471
				this.hide();
A
tslint  
Alex Dima 已提交
472
				if (stateChanged) {
473
					this.list.splice(0, this.list.length);
A
tslint  
Alex Dima 已提交
474
				}
475
				break;
J
Joao Moreno 已提交
476 477
			case State.Loading:
				this.messageElement.innerText = SuggestWidget.LOADING_MESSAGE;
J
Joao Moreno 已提交
478
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
479
				show(this.messageElement);
480
				this.show();
J
Joao Moreno 已提交
481 482 483
				break;
			case State.Empty:
				this.messageElement.innerText = SuggestWidget.NO_SUGGESTIONS_MESSAGE;
J
Joao Moreno 已提交
484
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
485
				show(this.messageElement);
486
				this.show();
J
Joao Moreno 已提交
487 488
				break;
			case State.Open:
J
Joao Moreno 已提交
489
				hide(this.messageElement, this.details.element);
J
Joao Moreno 已提交
490
				show(this.listElement);
491
				this.show();
J
Joao Moreno 已提交
492 493
				break;
			case State.Frozen:
J
Joao Moreno 已提交
494
				hide(this.messageElement, this.details.element);
J
Joao Moreno 已提交
495
				show(this.listElement);
496
				this.show();
J
Joao Moreno 已提交
497 498
				break;
			case State.Details:
J
Joao Moreno 已提交
499
				hide(this.messageElement, this.listElement);
J
Joao Moreno 已提交
500
				show(this.details.element);
501
				this.show();
502
				this._ariaAlert(this.details.getAriaLabel());
J
Joao Moreno 已提交
503 504 505
				break;
		}

506
		if (stateChanged) {
507 508
			this.editor.layoutContentWidget(this);
		}
509 510 511
	}

	private onDidTrigger(e: ITriggerEvent) {
J
Joao Moreno 已提交
512 513 514
		if (this.state !== State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
515

516
		this.telemetryTimer = this.telemetryService.timedPublicLog('suggestWidgetLoadingTime');
J
Joao Moreno 已提交
517 518 519 520 521 522 523 524 525 526 527 528 529
		this.isAuto = !!e.auto;

		if (!this.isAuto) {
			this.loadingTimeout = setTimeout(() => {
				this.loadingTimeout = null;
				this.setState(State.Loading);
			}, 50);
		}

		if (!e.retrigger) {
			this.telemetryData = {
				wasAutomaticallyTriggered: e.characterTriggered
			};
530 531
		}
	}
E
Erich Gamma 已提交
532

533
	private onDidSuggest(e: ISuggestEvent): void {
A
Alex Dima 已提交
534 535 536 537
		if (this.loadingTimeout) {
			clearTimeout(this.loadingTimeout);
			this.loadingTimeout = null;
		}
J
Joao Moreno 已提交
538

539
		this.completionModel = e.completionModel;
J
Joao Moreno 已提交
540

J
Joao Moreno 已提交
541
		if (e.isFrozen && this.state !== State.Empty) {
542 543
			this.setState(State.Frozen);
			return;
544 545
		}

546 547
		let visibleCount = this.completionModel.items.length;

J
Joao Moreno 已提交
548 549 550
		const isEmpty = visibleCount === 0;

		if (isEmpty) {
J
Joao Moreno 已提交
551 552 553 554 555 556 557 558 559 560
			if (e.auto) {
				this.setState(State.Hidden);
			} else {
				if (this.shouldShowEmptySuggestionList) {
					this.setState(State.Empty);
				} else {
					this.setState(State.Hidden);
				}
			}

J
Joao Moreno 已提交
561
			this.completionModel = null;
J
Joao Moreno 已提交
562

J
Joao Moreno 已提交
563 564 565 566 567
		} else {
			const currentWord = e.currentWord;
			const currentWordLowerCase = currentWord.toLowerCase();
			let bestSuggestionIndex = -1;
			let bestScore = -1;
J
Joao Moreno 已提交
568

J
Joao Moreno 已提交
569 570
			this.completionModel.items.forEach((item, index) => {
				const score = computeScore(item.suggestion.label, currentWord, currentWordLowerCase);
J
Joao Moreno 已提交
571

J
Joao Moreno 已提交
572 573 574 575 576
				if (score > bestScore) {
					bestScore = score;
					bestSuggestionIndex = index;
				}
			});
J
Joao Moreno 已提交
577

J
Joao Moreno 已提交
578 579 580 581
			this.telemetryData = this.telemetryData || {};
			this.telemetryData.suggestionCount = this.completionModel.items.length;
			this.telemetryData.suggestedIndex = bestSuggestionIndex;
			this.telemetryData.hintLength = currentWord.length;
J
Joao Moreno 已提交
582

J
Joao Moreno 已提交
583 584
			this.list.splice(0, this.list.length, ...this.completionModel.items);
			this.list.setFocus(bestSuggestionIndex);
J
Joao Moreno 已提交
585 586
			this.list.reveal(bestSuggestionIndex, 0);

J
Joao Moreno 已提交
587 588
			this.setState(State.Open);
		}
J
Joao Moreno 已提交
589

J
Joao Moreno 已提交
590 591 592 593 594
		if (this.telemetryTimer) {
			this.telemetryTimer.data = { reason: isEmpty ? 'empty' : 'results' };
			this.telemetryTimer.stop();
			this.telemetryTimer = null;
		}
595
	}
E
Erich Gamma 已提交
596

597
	private onDidCancel(e: ICancelEvent) {
A
Alex Dima 已提交
598 599 600 601
		if (this.loadingTimeout) {
			clearTimeout(this.loadingTimeout);
			this.loadingTimeout = null;
		}
E
Erich Gamma 已提交
602

603
		if (!e.retrigger) {
J
Joao Moreno 已提交
604
			this.setState(State.Hidden);
E
Erich Gamma 已提交
605

606 607 608 609
			if (this.telemetryData) {
				this.telemetryData.selectedIndex = -1;
				this.telemetryData.wasCancelled = true;
				this.submitTelemetryData();
E
Erich Gamma 已提交
610
			}
611
		}
E
Erich Gamma 已提交
612

613 614 615 616 617
		if (this.telemetryTimer) {
			this.telemetryTimer.data = { reason: 'cancel' };
			this.telemetryTimer.stop();
			this.telemetryTimer = null;
		}
E
Erich Gamma 已提交
618 619
	}

620
	selectNextPage(): boolean {
J
Joao Moreno 已提交
621 622 623 624 625 626 627 628 629 630 631 632
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
				this.details.pageDown();
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusNextPage();
				return true;
		}
E
Erich Gamma 已提交
633 634
	}

635
	selectNext(): boolean {
J
Joao Moreno 已提交
636 637 638 639 640 641 642 643 644 645 646 647
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
				this.details.scrollDown();
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusNext(1, true);
				return true;
		}
E
Erich Gamma 已提交
648 649
	}

650
	selectPreviousPage(): boolean {
J
Joao Moreno 已提交
651 652 653 654 655 656 657 658 659 660 661 662
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
				this.details.pageUp();
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusPreviousPage();
				return true;
		}
E
Erich Gamma 已提交
663 664
	}

665
	selectPrevious(): boolean {
J
Joao Moreno 已提交
666 667 668 669 670 671 672 673 674 675 676 677
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
				this.details.scrollUp();
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusPrevious(1, true);
				return true;
		}
E
Erich Gamma 已提交
678 679
	}

680
	acceptSelectedSuggestion(): boolean {
J
Joao Moreno 已提交
681 682 683 684 685 686 687 688 689 690 691 692 693 694
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Loading:
				return !this.isAuto;
			default:
				const focus = this.list.getFocus()[0];
				if (focus) {
					this.list.setSelection(this.completionModel.items.indexOf(focus));
				} else {
					this.model.cancel();
				}
				return true;
		}
E
Erich Gamma 已提交
695 696
	}

697
	toggleDetails(): void {
J
Joao Moreno 已提交
698 699 700 701 702
		if (this.state === State.Details) {
			this.setState(State.Open);
			this.editor.focus();
			return;
		}
J
Joao Moreno 已提交
703

J
Joao Moreno 已提交
704 705 706
		if (this.state !== State.Open) {
			return;
		}
J
Joao Moreno 已提交
707

J
Joao Moreno 已提交
708
		const item = this.list.getFocus()[0];
J
Joao Moreno 已提交
709

J
Joao Moreno 已提交
710 711 712
		if (!item || !item.suggestion.documentationLabel) {
			return;
		}
J
Joao Moreno 已提交
713

J
Joao Moreno 已提交
714 715
		this.setState(State.Details);
		this.editor.focus();
J
Joao Moreno 已提交
716 717
	}

718 719
	private show(): void {
		this.updateWidgetHeight();
J
Joao Moreno 已提交
720
		this._onDidVisibilityChange.fire(true);
J
Joao Moreno 已提交
721
		this.renderDetails();
J
Joao Moreno 已提交
722
		this.showTimeout = TPromise.timeout(100).then(() => {
J
Joao Moreno 已提交
723
			addClass(this.element, 'visible');
E
Erich Gamma 已提交
724 725 726
		});
	}

727
	private hide(): void {
J
Joao Moreno 已提交
728
		this._onDidVisibilityChange.fire(false);
J
Joao Moreno 已提交
729
		removeClass(this.element, 'visible');
E
Erich Gamma 已提交
730 731
	}

732
	cancel(): void {
J
Joao Moreno 已提交
733
		if (this.state === State.Details) {
J
Joao Moreno 已提交
734
			this.toggleDetails();
J
Joao Moreno 已提交
735 736 737
		} else {
			this.model.cancel();
		}
J
Joao Moreno 已提交
738 739
	}

740
	getPosition(): IContentWidgetPosition {
J
Joao Moreno 已提交
741 742
		if (this.state === State.Hidden) {
			return null;
E
Erich Gamma 已提交
743
		}
J
Joao Moreno 已提交
744 745 746

		return {
			position: this.editor.getPosition(),
A
Alex Dima 已提交
747
			preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE]
J
Joao Moreno 已提交
748
		};
E
Erich Gamma 已提交
749 750
	}

751
	getDomNode(): HTMLElement {
E
Erich Gamma 已提交
752 753 754
		return this.element;
	}

755
	getId(): string {
E
Erich Gamma 已提交
756 757 758
		return SuggestWidget.ID;
	}

J
Johannes Rieken 已提交
759
	private submitTelemetryData(): void {
E
Erich Gamma 已提交
760 761 762 763
		this.telemetryService.publicLog('suggestWidget', this.telemetryData);
		this.telemetryData = null;
	}

J
Joao Moreno 已提交
764
	private updateWidgetHeight(): number {
J
Joao Moreno 已提交
765
		let height = 0;
E
Erich Gamma 已提交
766

J
Joao Moreno 已提交
767
		if (this.state === State.Empty || this.state === State.Loading) {
768
			height = this.lineHeight;
J
Joao Moreno 已提交
769
		} else if (this.state === State.Details) {
770
			height = 12 * this.lineHeight;
J
Joao Moreno 已提交
771
		} else {
J
Joao Moreno 已提交
772
			const focus = this.list.getFocus()[0];
773
			const focusHeight = focus ? this.getHeight(focus) : this.lineHeight;
J
Joao Moreno 已提交
774
			height = focusHeight;
J
Joao Moreno 已提交
775

776 777
			const suggestionCount = (this.list.contentHeight - focusHeight) / this.lineHeight;
			height += Math.min(suggestionCount, 11) * this.lineHeight;
J
Joao Moreno 已提交
778
		}
J
Joao Moreno 已提交
779

E
Erich Gamma 已提交
780
		this.element.style.height = height + 'px';
J
Joao Moreno 已提交
781
		this.list.layout(height);
E
Erich Gamma 已提交
782 783
		this.editor.layoutContentWidget(this);

J
Joao Moreno 已提交
784
		return height;
J
Joao Moreno 已提交
785 786
	}

J
Joao Moreno 已提交
787 788 789 790 791 792 793
	private renderDetails(): void {
		if (this.state !== State.Details) {
			this.details.render(null);
		} else {
			this.details.render(this.list.getFocus()[0]);
		}
	}
J
Joao Moreno 已提交
794

795 796 797 798
	// IDelegate

	private get lineHeight(): number {
		const { fontSize } = this.editor.getConfiguration();
J
Joao Moreno 已提交
799
		return Math.floor(fontSize * 1.6);
800 801 802 803 804 805
	}

	getHeight(element: CompletionItem): number {
		const focus = this.list.getFocus()[0];

		if (element.suggestion.documentationLabel && element === focus) {
J
Joao Moreno 已提交
806
			return Math.floor(this.lineHeight * 1.8);
807 808 809 810 811 812 813 814 815 816
		}

		return this.lineHeight;
	}

	getTemplateId(element: CompletionItem): string {
		return 'suggestion';
	}

	dispose(): void {
J
Joao Moreno 已提交
817 818 819
		this.state = null;
		this.suggestionSupportsAutoAccept = null;
		this.currentSuggestionDetails = null;
J
Joao Moreno 已提交
820
		this.focusedItem = null;
J
Joao Moreno 已提交
821 822 823 824 825
		this.telemetryData = null;
		this.telemetryService = null;
		this.telemetryTimer = null;
		this.element = null;
		this.messageElement = null;
J
Joao Moreno 已提交
826
		this.listElement = null;
J
Joao Moreno 已提交
827 828
		this.details.dispose();
		this.details = null;
J
Joao Moreno 已提交
829 830
		this.list.dispose();
		this.list = null;
J
Joao Moreno 已提交
831
		this.toDispose = dispose(this.toDispose);
J
Joao Moreno 已提交
832 833
		this._onDidVisibilityChange.dispose();
		this._onDidVisibilityChange = null;
A
Alex Dima 已提交
834 835 836 837
		if (this.loadingTimeout) {
			clearTimeout(this.loadingTimeout);
			this.loadingTimeout = null;
		}
J
Joao Moreno 已提交
838 839 840 841 842 843 844 845 846 847

		if (this.editorBlurTimeout) {
			this.editorBlurTimeout.cancel();
			this.editorBlurTimeout = null;
		}

		if (this.showTimeout) {
			this.showTimeout.cancel();
			this.showTimeout = null;
		}
E
Erich Gamma 已提交
848
	}
J
Johannes Rieken 已提交
849
}