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

'use strict';

8
import 'vs/css!./media/suggest';
J
Johannes Rieken 已提交
9
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
10
import { createMatches } from 'vs/base/common/filters';
11
import * as strings from 'vs/base/common/strings';
J
Joao Moreno 已提交
12
import Event, { Emitter, chain } from 'vs/base/common/event';
J
Joao Moreno 已提交
13
import { TPromise } from 'vs/base/common/winjs.base';
J
Johannes Rieken 已提交
14
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
J
Joao Moreno 已提交
15
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
16
import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass } from 'vs/base/browser/dom';
J
Joao Moreno 已提交
17
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
J
Joao Moreno 已提交
18
import { IDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list';
J
Joao Moreno 已提交
19 20 21
import { List } from 'vs/base/browser/ui/list/listWidget';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Johannes Rieken 已提交
22 23
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
24
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
J
Joao Moreno 已提交
25
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
26 27
import { Context as SuggestContext } from './suggest';
import { ICompletionItem, CompletionModel } from './completionModel';
J
Johannes Rieken 已提交
28
import { alert } from 'vs/base/browser/ui/aria/aria';
J
Joao Moreno 已提交
29
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
30 31
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { IThemeService, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
32
import { registerColor, editorWidgetBackground, listFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
33
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
E
Erich Gamma 已提交
34

J
Joao Moreno 已提交
35
const sticky = false; // for development purposes
36
const expandSuggestionDocsByDefault = false;
J
Joao Moreno 已提交
37

E
Erich Gamma 已提交
38 39 40 41
interface ISuggestionTemplateData {
	root: HTMLElement;
	icon: HTMLElement;
	colorspan: HTMLElement;
A
Alex Dima 已提交
42
	highlightedLabel: HighlightedLabel;
43
	typeLabel: HTMLElement;
44
	readMore: HTMLElement;
45
	disposables: IDisposable[];
E
Erich Gamma 已提交
46 47
}

M
Martin Aeschlimann 已提交
48 49 50
/**
 * Suggest widget colors
 */
51
export const editorSuggestWidgetBackground = registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.'));
52
export const editorSuggestWidgetBorder = registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.'));
53 54 55
export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hc: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.'));
export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: listFocusBackground, light: listFocusBackground, hc: listFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.'));
export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.'));
56

M
Martin Aeschlimann 已提交
57

58
const colorRegExp = /^(#([\da-f]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))$/i;
59 60 61
function matchesColor(text: string) {
	return text && text.match(colorRegExp) ? text : null;
}
62

63 64 65 66 67
function canExpandCompletionItem(item: ICompletionItem) {
	const suggestion = item.suggestion;
	if (suggestion.documentation) {
		return true;
	}
68
	return (suggestion.detail && suggestion.detail !== suggestion.label);
69 70
}

71
class Renderer implements IRenderer<ICompletionItem, ISuggestionTemplateData> {
E
Erich Gamma 已提交
72

J
Joao Moreno 已提交
73 74
	constructor(
		private widget: SuggestWidget,
75
		private editor: ICodeEditor,
76
		private triggerKeybindingLabel: string
J
Joao Moreno 已提交
77
	) {
78

J
Joao Moreno 已提交
79
	}
J
Joao Moreno 已提交
80

J
Joao Moreno 已提交
81 82 83
	get templateId(): string {
		return 'suggestion';
	}
E
Erich Gamma 已提交
84

J
Joao Moreno 已提交
85
	renderTemplate(container: HTMLElement): ISuggestionTemplateData {
J
Johannes Rieken 已提交
86
		const data = <ISuggestionTemplateData>Object.create(null);
87
		data.disposables = [];
E
Erich Gamma 已提交
88
		data.root = container;
J
Joao Moreno 已提交
89

J
Joao Moreno 已提交
90 91
		data.icon = append(container, $('.icon'));
		data.colorspan = append(data.icon, $('span.colorspan'));
E
Erich Gamma 已提交
92

J
Joao Moreno 已提交
93
		const text = append(container, $('.contents'));
J
Joao Moreno 已提交
94
		const main = append(text, $('.main'));
A
Alex Dima 已提交
95
		data.highlightedLabel = new HighlightedLabel(main);
96
		data.disposables.push(data.highlightedLabel);
97 98
		data.typeLabel = append(main, $('span.type-label'));

99 100
		data.readMore = append(main, $('span.readMore'));
		data.readMore.title = nls.localize('readMore', "Read More...{0}", this.triggerKeybindingLabel);
101

102
		const configureFont = () => {
J
Joao Moreno 已提交
103 104 105 106
			const configuration = this.editor.getConfiguration();
			const fontFamily = configuration.fontInfo.fontFamily;
			const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize;
			const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
J
Johannes Rieken 已提交
107 108
			const fontSizePx = `${fontSize}px`;
			const lineHeightPx = `${lineHeight}px`;
J
Joao Moreno 已提交
109 110 111 112 113 114

			data.root.style.fontSize = fontSizePx;
			main.style.fontFamily = fontFamily;
			main.style.lineHeight = lineHeightPx;
			data.icon.style.height = lineHeightPx;
			data.icon.style.width = lineHeightPx;
115 116
			data.readMore.style.height = lineHeightPx;
			data.readMore.style.width = lineHeightPx;
117 118 119 120
		};

		configureFont();

J
Joao Moreno 已提交
121
		chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
J
Joao Moreno 已提交
122
			.filter(e => e.fontInfo || e.contribInfo)
J
Joao Moreno 已提交
123
			.on(configureFont, null, data.disposables);
124

E
Erich Gamma 已提交
125 126 127
		return data;
	}

128
	renderElement(element: ICompletionItem, index: number, templateData: ISuggestionTemplateData): void {
J
Johannes Rieken 已提交
129
		const data = <ISuggestionTemplateData>templateData;
130
		const suggestion = (<ICompletionItem>element).suggestion;
E
Erich Gamma 已提交
131

132
		if (canExpandCompletionItem(element)) {
133 134 135 136 137
			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));
		}

138 139 140 141
		data.icon.className = 'icon ' + suggestion.type;
		data.colorspan.style.backgroundColor = '';

		if (suggestion.type === 'color') {
142
			let color = matchesColor(suggestion.label) || matchesColor(suggestion.documentation);
143 144 145 146
			if (color) {
				data.icon.className = 'icon customcolor';
				data.colorspan.style.backgroundColor = color;
			}
E
Erich Gamma 已提交
147 148
		}

J
Johannes Rieken 已提交
149
		data.highlightedLabel.set(suggestion.label, createMatches(element.matches));
150
		data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, '');
J
Joao Moreno 已提交
151

152
		if (canExpandCompletionItem(element)) {
153 154
			show(data.readMore);
			data.readMore.onmousedown = e => {
155 156 157
				e.stopPropagation();
				e.preventDefault();
			};
158
			data.readMore.onclick = e => {
159 160 161 162 163
				e.stopPropagation();
				e.preventDefault();
				this.widget.toggleDetails();
			};
		} else {
164 165 166
			hide(data.readMore);
			data.readMore.onmousedown = null;
			data.readMore.onclick = null;
167
		}
J
Joao Moreno 已提交
168

E
Erich Gamma 已提交
169 170
	}

J
Joao Moreno 已提交
171 172
	disposeTemplate(templateData: ISuggestionTemplateData): void {
		templateData.highlightedLabel.dispose();
173
		templateData.disposables = dispose(templateData.disposables);
J
Joao Moreno 已提交
174 175
	}
}
E
Erich Gamma 已提交
176

A
Alex Dima 已提交
177
const enum State {
J
Joao Moreno 已提交
178 179 180
	Hidden,
	Loading,
	Empty,
J
Joao Moreno 已提交
181
	Open,
J
Joao Moreno 已提交
182 183 184 185 186 187 188
	Frozen,
	Details
}

class SuggestionDetails {

	private el: HTMLElement;
189
	private close: HTMLElement;
190
	private scrollbar: DomScrollableElement;
191
	private body: HTMLElement;
J
Joao Moreno 已提交
192 193
	private type: HTMLElement;
	private docs: HTMLElement;
194 195 196 197 198 199
	private ariaLabel: string;
	private disposables: IDisposable[];

	constructor(
		container: HTMLElement,
		private widget: SuggestWidget,
200 201
		private editor: ICodeEditor,
		private triggerKeybindingLabel: string
202 203
	) {
		this.disposables = [];
J
Joao Moreno 已提交
204 205

		this.el = append(container, $('.details'));
206 207
		this.disposables.push(toDisposable(() => container.removeChild(this.el)));

208
		this.body = $('.body');
209

210
		this.scrollbar = new DomScrollableElement(this.body, { canUseTranslate3d: false });
211
		append(this.el, this.scrollbar.getDomNode());
212 213
		this.disposables.push(this.scrollbar);

214
		this.close = append(this.body, $('span.close'));
215
		this.close.title = nls.localize('readLess', "Read less...{0}", triggerKeybindingLabel);
216 217
		this.type = append(this.body, $('p.type'));

218

219
		this.docs = append(this.body, $('p.docs'));
220
		this.ariaLabel = null;
221 222 223

		this.configureFont();

J
Joao Moreno 已提交
224 225 226
		chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
			.filter(e => e.fontInfo)
			.on(this.configureFont, this, this.disposables);
J
Joao Moreno 已提交
227 228
	}

J
Joao Moreno 已提交
229 230 231
	get element() {
		return this.el;
	}
J
Joao Moreno 已提交
232

233
	render(item: ICompletionItem): void {
234
		if (!item || !canExpandCompletionItem(item)) {
J
Joao Moreno 已提交
235 236
			this.type.textContent = '';
			this.docs.textContent = '';
237
			addClass(this.el, 'no-docs');
238
			this.ariaLabel = null;
J
Joao Moreno 已提交
239 240
			return;
		}
241
		removeClass(this.el, 'no-docs');
J
Joao Moreno 已提交
242
		this.type.innerText = item.suggestion.detail || '';
J
Joao Moreno 已提交
243
		this.docs.textContent = item.suggestion.documentation;
244

245
		this.el.style.height = this.type.offsetHeight + this.docs.offsetHeight + 'px';
246

247
		this.close.onmousedown = e => {
248 249 250
			e.preventDefault();
			e.stopPropagation();
		};
251
		this.close.onclick = e => {
252 253 254 255
			e.preventDefault();
			e.stopPropagation();
			this.widget.toggleDetails();
		};
256

J
Joao Moreno 已提交
257
		this.body.scrollTop = 0;
258
		this.scrollbar.scanDomNode();
259

260
		this.ariaLabel = strings.format('{0}\n{1}\n{2}', item.suggestion.label || '', item.suggestion.detail || '', item.suggestion.documentation || '');
261 262 263 264
	}

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

267 268 269 270 271 272 273 274
	scrollDown(much = 8): void {
		this.body.scrollTop += much;
	}

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

275 276 277 278 279 280 281 282
	scrollTop(): void {
		this.body.scrollTop = 0;
	}

	scrollBottom(): void {
		this.body.scrollTop = this.body.scrollHeight;
	}

283 284 285 286 287 288 289 290
	pageDown(): void {
		this.scrollDown(80);
	}

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

291
	private configureFont() {
J
Joao Moreno 已提交
292 293 294
		const configuration = this.editor.getConfiguration();
		const fontFamily = configuration.fontInfo.fontFamily;
		const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize;
295
		const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
J
Johannes Rieken 已提交
296
		const fontSizePx = `${fontSize}px`;
297
		const lineHeightPx = `${lineHeight}px`;
J
Joao Moreno 已提交
298

J
Joao Moreno 已提交
299 300
		this.el.style.fontSize = fontSizePx;
		this.type.style.fontFamily = fontFamily;
301 302
		this.close.style.height = lineHeightPx;
		this.close.style.width = lineHeightPx;
303
	}
304

305 306
	dispose(): void {
		this.disposables = dispose(this.disposables);
J
Joao Moreno 已提交
307
	}
J
Joao Moreno 已提交
308 309
}

310
export class SuggestWidget implements IContentWidget, IDelegate<ICompletionItem>, IDisposable {
J
Joao Moreno 已提交
311

312
	private static ID: string = 'editor.widget.suggestWidget';
E
Erich Gamma 已提交
313

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

J
Joao Moreno 已提交
317
	// Editor.IContentWidget.allowEditorOverflow
318
	readonly allowEditorOverflow = true;
J
Joao Moreno 已提交
319

J
Joao Moreno 已提交
320
	private state: State;
E
Erich Gamma 已提交
321
	private isAuto: boolean;
322
	private loadingTimeout: number;
J
Joao Moreno 已提交
323
	private currentSuggestionDetails: TPromise<void>;
324
	private focusedItemIndex: number;
325
	private focusedItem: ICompletionItem;
326
	private ignoreFocusEvents = false;
J
Joao Moreno 已提交
327
	private completionModel: CompletionModel;
J
Joao Moreno 已提交
328

E
Erich Gamma 已提交
329
	private element: HTMLElement;
J
Joao Moreno 已提交
330
	private messageElement: HTMLElement;
J
Joao Moreno 已提交
331
	private listElement: HTMLElement;
J
Joao Moreno 已提交
332
	private details: SuggestionDetails;
333
	private list: List<ICompletionItem>;
E
Erich Gamma 已提交
334

A
Alex Dima 已提交
335 336 337
	private suggestWidgetVisible: IContextKey<boolean>;
	private suggestWidgetMultipleSuggestions: IContextKey<boolean>;
	private suggestionSupportsAutoAccept: IContextKey<boolean>;
J
Joao Moreno 已提交
338

J
Joao Moreno 已提交
339 340
	private editorBlurTimeout: TPromise<void>;
	private showTimeout: TPromise<void>;
J
Joao Moreno 已提交
341
	private toDispose: IDisposable[];
J
Joao Moreno 已提交
342

343 344
	private onDidSelectEmitter = new Emitter<ICompletionItem>();
	private onDidFocusEmitter = new Emitter<ICompletionItem>();
345 346 347
	private onDidHideEmitter = new Emitter<this>();
	private onDidShowEmitter = new Emitter<this>();

348 349 350

	readonly onDidSelect: Event<ICompletionItem> = this.onDidSelectEmitter.event;
	readonly onDidFocus: Event<ICompletionItem> = this.onDidFocusEmitter.event;
351 352
	readonly onDidHide: Event<this> = this.onDidHideEmitter.event;
	readonly onDidShow: Event<this> = this.onDidShowEmitter.event;
353

354
	private readonly maxWidgetWidth = 660;
355
	private readonly listWidth = 330;
356
	private storageService: IStorageService;
357 358
	private detailsFocusBorderColor: string;
	private detailsBorderColor: string;
359

360
	constructor(
A
Alex Dima 已提交
361
		private editor: ICodeEditor,
J
Joao Moreno 已提交
362
		@ITelemetryService private telemetryService: ITelemetryService,
363
		@IContextKeyService contextKeyService: IContextKeyService,
B
Benjamin Pasero 已提交
364
		@IInstantiationService instantiationService: IInstantiationService,
365 366 367
		@IThemeService themeService: IThemeService,
		@IStorageService storageService: IStorageService,
		@IKeybindingService keybindingService: IKeybindingService
368
	) {
369 370 371
		const kb = keybindingService.lookupKeybinding('editor.action.triggerSuggest');
		const triggerKeybindingLabel = !kb ? '' : ` (${kb.getLabel()})`;

E
Erich Gamma 已提交
372
		this.isAuto = false;
J
Joao Moreno 已提交
373
		this.focusedItem = null;
374
		this.storageService = storageService;
J
Joao Moreno 已提交
375
		this.element = $('.editor-widget.suggest-widget');
E
Erich Gamma 已提交
376

A
Alex Dima 已提交
377
		if (!this.editor.getConfiguration().contribInfo.iconsInSuggestions) {
J
Joao Moreno 已提交
378
			addClass(this.element, 'no-icons');
E
Erich Gamma 已提交
379 380
		}

J
Joao Moreno 已提交
381
		this.messageElement = append(this.element, $('.message'));
J
Joao Moreno 已提交
382
		this.listElement = append(this.element, $('.tree'));
383
		this.details = new SuggestionDetails(this.element, this, this.editor, triggerKeybindingLabel);
J
Joao Moreno 已提交
384

385
		let renderer: IRenderer<ICompletionItem, any> = instantiationService.createInstance(Renderer, this, this.editor, triggerKeybindingLabel);
J
Joao Moreno 已提交
386

387 388 389 390
		this.list = new List(this.listElement, this, [renderer], {
			useShadows: false,
			selectOnMouseDown: true
		});
J
Joao Moreno 已提交
391 392

		this.toDispose = [
393 394 395 396
			attachListStyler(this.list, themeService, {
				listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,
				listInactiveFocusOutline: activeContrastBorder
			}),
M
Martin Aeschlimann 已提交
397
			themeService.onThemeChange(t => this.onThemeChange(t)),
A
Alex Dima 已提交
398
			editor.onDidBlurEditorText(() => this.onEditorBlur()),
J
Joao Moreno 已提交
399
			this.list.onSelectionChange(e => this.onListSelection(e)),
J
Joao Moreno 已提交
400
			this.list.onFocusChange(e => this.onListFocus(e)),
401
			this.editor.onDidChangeCursorSelection(() => this.onCursorSelectionChanged())
J
Joao Moreno 已提交
402 403
		];

404 405 406
		this.suggestWidgetVisible = SuggestContext.Visible.bindTo(contextKeyService);
		this.suggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(contextKeyService);
		this.suggestionSupportsAutoAccept = SuggestContext.AcceptOnKey.bindTo(contextKeyService);
J
Joao Moreno 已提交
407

J
Joao Moreno 已提交
408 409
		this.editor.addContentWidget(this);
		this.setState(State.Hidden);
410

M
Martin Aeschlimann 已提交
411 412
		this.onThemeChange(themeService.getTheme());

413 414 415 416 417 418 419 420 421 422 423 424 425
		// 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 已提交
426
	}
E
Erich Gamma 已提交
427

J
Joao Moreno 已提交
428 429 430 431
	private onCursorSelectionChanged(): void {
		if (this.state === State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
432

J
Joao Moreno 已提交
433 434
		this.editor.layoutContentWidget(this);
	}
J
Joao Moreno 已提交
435

J
Joao Moreno 已提交
436
	private onEditorBlur(): void {
J
Joao Moreno 已提交
437 438 439 440
		if (sticky) {
			return;
		}

J
Joao Moreno 已提交
441
		this.editorBlurTimeout = TPromise.timeout(150).then(() => {
J
Joao Moreno 已提交
442
			if (!this.editor.isFocused()) {
J
Joao Moreno 已提交
443 444 445 446 447
				this.setState(State.Hidden);
			}
		});
	}

J
Joao Moreno 已提交
448
	private onListSelection(e: IListEvent<ICompletionItem>): void {
J
Joao Moreno 已提交
449 450 451
		if (!e.elements.length) {
			return;
		}
E
Erich Gamma 已提交
452

J
Joao Moreno 已提交
453
		const item = e.elements[0];
454
		this.onDidSelectEmitter.fire(item);
J
Joao Moreno 已提交
455 456

		alert(nls.localize('suggestionAriaAccepted', "{0}, accepted", item.suggestion.label));
J
Johannes Rieken 已提交
457 458

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

J
Johannes Rieken 已提交
461
	private _getSuggestionAriaAlertLabel(item: ICompletionItem): string {
462
		if (canExpandCompletionItem(item)) {
J
Johannes Rieken 已提交
463
			return nls.localize('ariaCurrentSuggestionWithDetails', "{0}, suggestion, has details", item.suggestion.label);
464
		} else {
J
Johannes Rieken 已提交
465
			return nls.localize('ariaCurrentSuggestion', "{0}, suggestion", item.suggestion.label);
466 467 468 469
		}
	}

	private _lastAriaAlertLabel: string;
J
Johannes Rieken 已提交
470
	private _ariaAlert(newAriaAlertLabel: string): void {
471 472 473 474 475 476 477 478 479
		if (this._lastAriaAlertLabel === newAriaAlertLabel) {
			return;
		}
		this._lastAriaAlertLabel = newAriaAlertLabel;
		if (this._lastAriaAlertLabel) {
			alert(this._lastAriaAlertLabel);
		}
	}

M
Martin Aeschlimann 已提交
480 481 482
	private onThemeChange(theme: ITheme) {
		let backgroundColor = theme.getColor(editorSuggestWidgetBackground);
		if (backgroundColor) {
483 484
			this.listElement.style.backgroundColor = backgroundColor.toString();
			this.details.element.style.backgroundColor = backgroundColor.toString();
R
Ramya Achutha Rao 已提交
485
			this.messageElement.style.backgroundColor = backgroundColor.toString();
M
Martin Aeschlimann 已提交
486 487 488
		}
		let borderColor = theme.getColor(editorSuggestWidgetBorder);
		if (borderColor) {
489 490 491
			this.listElement.style.borderColor = borderColor.toString();
			this.details.element.style.borderColor = borderColor.toString();
			this.messageElement.style.borderColor = borderColor.toString();
492
			this.detailsBorderColor = borderColor.toString();
M
Martin Aeschlimann 已提交
493
		}
494 495 496 497
		let focusBorderColor = theme.getColor(focusBorder);
		if (focusBorderColor) {
			this.detailsFocusBorderColor = focusBorderColor.toString();
		}
M
Martin Aeschlimann 已提交
498 499
	}

J
Joao Moreno 已提交
500
	private onListFocus(e: IListEvent<ICompletionItem>): void {
501 502 503 504
		if (this.ignoreFocusEvents) {
			return;
		}

J
Joao Moreno 已提交
505
		if (!e.elements.length) {
J
Joao Moreno 已提交
506 507 508 509 510
			if (this.currentSuggestionDetails) {
				this.currentSuggestionDetails.cancel();
				this.currentSuggestionDetails = null;
				this.focusedItem = null;
			}
511

J
Joao Moreno 已提交
512
			this._ariaAlert(null);
513 514 515
			// TODO@Alex: Chromium bug
			// this.editor.setAriaActiveDescendant(null);

J
Joao Moreno 已提交
516 517
			return;
		}
E
Erich Gamma 已提交
518

J
Joao Moreno 已提交
519
		const item = e.elements[0];
520
		this._ariaAlert(this._getSuggestionAriaAlertLabel(item));
521 522 523 524 525 526

		// 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 已提交
527

J
Joao Moreno 已提交
528 529 530
		if (item === this.focusedItem) {
			return;
		}
E
Erich Gamma 已提交
531

J
Joao Moreno 已提交
532 533 534 535 536
		if (this.currentSuggestionDetails) {
			this.currentSuggestionDetails.cancel();
			this.currentSuggestionDetails = null;
		}

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

539
		this.suggestionSupportsAutoAccept.set(!item.suggestion.noAutoAccept);
540 541 542 543

		const oldFocus = this.focusedItem;
		const oldFocusIndex = this.focusedItemIndex;
		this.focusedItemIndex = index;
J
Joao Moreno 已提交
544
		this.focusedItem = item;
545 546 547 548 549 550 551

		if (oldFocus) {
			this.ignoreFocusEvents = true;
			this.list.splice(oldFocusIndex, 1, [oldFocus]);
			this.ignoreFocusEvents = false;
		}

552
		this.updateListHeight();
J
Joao Moreno 已提交
553
		this.list.reveal(index);
J
Joao Moreno 已提交
554

555 556
		this.currentSuggestionDetails = item.resolve()
			.then(() => {
557 558 559 560
				this.ignoreFocusEvents = true;
				this.list.splice(index, 1, [item]);
				this.ignoreFocusEvents = false;

J
Joao Moreno 已提交
561
				this.list.setFocus([index]);
J
Joao Moreno 已提交
562
				this.list.reveal(index);
563

564
				if (this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault)) {
565
					this.showDetails();
566

567
					this._ariaAlert(this.details.getAriaLabel());
568
				} else {
569
					removeClass(this.element, 'docs-side');
570
				}
J
Joao Moreno 已提交
571 572 573
			})
			.then(null, err => !isPromiseCanceledError(err) && onUnexpectedError(err))
			.then(() => this.currentSuggestionDetails = null);
574 575 576

		// emit an event
		this.onDidFocusEmitter.fire(item);
J
Joao Moreno 已提交
577
	}
J
Joao Moreno 已提交
578 579

	private setState(state: State): void {
J
Joao Moreno 已提交
580 581 582 583
		if (!this.element) {
			return;
		}

584
		const stateChanged = this.state !== state;
J
Joao Moreno 已提交
585 586
		this.state = state;

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

J
Joao Moreno 已提交
589 590
		switch (state) {
			case State.Hidden:
J
Joao Moreno 已提交
591
				hide(this.messageElement, this.details.element);
J
Joao Moreno 已提交
592
				show(this.listElement);
J
Joao Moreno 已提交
593
				this.hide();
A
tslint  
Alex Dima 已提交
594
				if (stateChanged) {
595
					this.list.splice(0, this.list.length);
A
tslint  
Alex Dima 已提交
596
				}
597
				break;
J
Joao Moreno 已提交
598
			case State.Loading:
J
Joao Moreno 已提交
599
				this.messageElement.textContent = SuggestWidget.LOADING_MESSAGE;
J
Joao Moreno 已提交
600
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
601
				show(this.messageElement);
602
				this.show();
J
Joao Moreno 已提交
603 604
				break;
			case State.Empty:
J
Joao Moreno 已提交
605
				this.messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE;
J
Joao Moreno 已提交
606
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
607
				show(this.messageElement);
608
				this.show();
J
Joao Moreno 已提交
609 610
				break;
			case State.Open:
611
				hide(this.messageElement);
612
				show(this.listElement);
613
				if (this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault)) {
614
					show(this.details.element);
615
					this.expandSideOrBelow();
616 617
				} else {
					hide(this.details.element);
618
				}
619
				this.show();
J
Joao Moreno 已提交
620 621
				break;
			case State.Frozen:
J
Joao Moreno 已提交
622
				hide(this.messageElement, this.details.element);
J
Joao Moreno 已提交
623
				show(this.listElement);
624
				this.show();
J
Joao Moreno 已提交
625 626
				break;
			case State.Details:
627 628
				hide(this.messageElement);
				show(this.details.element, this.listElement);
629
				this.show();
630
				this._ariaAlert(this.details.getAriaLabel());
J
Joao Moreno 已提交
631 632 633
				break;
		}

634
		if (stateChanged) {
635 636
			this.editor.layoutContentWidget(this);
		}
637 638
	}

639
	showTriggered(auto: boolean) {
J
Joao Moreno 已提交
640 641 642
		if (this.state !== State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
643

644
		this.isAuto = !!auto;
J
Joao Moreno 已提交
645 646 647 648 649 650 651

		if (!this.isAuto) {
			this.loadingTimeout = setTimeout(() => {
				this.loadingTimeout = null;
				this.setState(State.Loading);
			}, 50);
		}
652
	}
E
Erich Gamma 已提交
653

654
	showSuggestions(completionModel: CompletionModel, isFrozen: boolean, isAuto: boolean): void {
A
Alex Dima 已提交
655 656 657 658
		if (this.loadingTimeout) {
			clearTimeout(this.loadingTimeout);
			this.loadingTimeout = null;
		}
J
Joao Moreno 已提交
659

660
		this.completionModel = completionModel;
J
Joao Moreno 已提交
661

R
Ramya Achutha Rao 已提交
662
		if (isFrozen && this.state !== State.Empty && this.state !== State.Hidden) {
663 664
			this.setState(State.Frozen);
			return;
665 666
		}

667 668
		let visibleCount = this.completionModel.items.length;

J
Joao Moreno 已提交
669
		const isEmpty = visibleCount === 0;
670
		this.suggestWidgetMultipleSuggestions.set(visibleCount > 1);
J
Joao Moreno 已提交
671 672

		if (isEmpty) {
673
			if (isAuto) {
J
Joao Moreno 已提交
674 675
				this.setState(State.Hidden);
			} else {
Y
Yuki Ueda 已提交
676
				this.setState(State.Empty);
J
Joao Moreno 已提交
677 678
			}

J
Joao Moreno 已提交
679
			this.completionModel = null;
J
Joao Moreno 已提交
680

J
Joao Moreno 已提交
681
		} else {
J
Joao Moreno 已提交
682
			const { stats } = this.completionModel;
683
			stats['wasAutomaticallyTriggered'] = !!isAuto;
684
			this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() });
J
Joao Moreno 已提交
685

J
Joao Moreno 已提交
686 687
			this.focusedItem = null;
			this.focusedItemIndex = null;
J
Joao Moreno 已提交
688
			this.list.splice(0, this.list.length, this.completionModel.items);
689 690
			this.list.setFocus([0]);
			this.list.reveal(0, 0);
J
Joao Moreno 已提交
691

R
Ramya Achutha Rao 已提交
692 693 694 695 696
			if (isFrozen) {
				this.setState(State.Frozen);
			} else {
				this.setState(State.Open);
			}
J
Joao Moreno 已提交
697
		}
698
	}
E
Erich Gamma 已提交
699

J
Joao Moreno 已提交
700
	selectNextPage(): boolean {
J
Joao Moreno 已提交
701 702 703 704 705 706 707 708 709 710 711 712
		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 已提交
713 714
	}

J
Joao Moreno 已提交
715
	selectNext(): boolean {
J
Joao Moreno 已提交
716 717 718 719 720 721 722 723 724
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusNext(1, true);
				return true;
		}
E
Erich Gamma 已提交
725 726
	}

727 728 729 730 731
	selectLast(): boolean {
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
732
				this.details.scrollBottom();
733 734 735 736 737 738 739 740 741
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusLast();
				return true;
		}
	}

J
Joao Moreno 已提交
742
	selectPreviousPage(): boolean {
J
Joao Moreno 已提交
743 744 745 746 747 748 749 750 751 752 753 754
		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 已提交
755 756
	}

J
Joao Moreno 已提交
757
	selectPrevious(): boolean {
J
Joao Moreno 已提交
758 759 760 761 762 763 764
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusPrevious(1, true);
J
Joao Moreno 已提交
765
				return false;
J
Joao Moreno 已提交
766
		}
E
Erich Gamma 已提交
767 768
	}

769 770 771 772 773
	selectFirst(): boolean {
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
774
				this.details.scrollTop();
775 776 777 778 779 780 781 782 783
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusFirst();
				return true;
		}
	}

784
	getFocusedItem(): ICompletionItem {
785 786 787 788 789
		if (this.state !== State.Hidden
			&& this.state !== State.Empty
			&& this.state !== State.Loading) {

			return this.list.getFocusedElements()[0];
J
Joao Moreno 已提交
790
		}
791
		return undefined;
E
Erich Gamma 已提交
792 793
	}

794
	toggleDetailsFocus(): void {
J
Joao Moreno 已提交
795 796
		if (this.state === State.Details) {
			this.setState(State.Open);
797 798 799
			if (this.detailsBorderColor) {
				this.details.element.style.borderColor = this.detailsBorderColor;
			}
800
		} else if (this.state === State.Open
801
			&& this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault)) {
802
			this.setState(State.Details);
803 804 805
			if (this.detailsFocusBorderColor) {
				this.details.element.style.borderColor = this.detailsFocusBorderColor;
			}
J
Joao Moreno 已提交
806
		}
807
	}
J
Joao Moreno 已提交
808

809
	toggleDetails(): void {
810

811
		if (this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault)) {
812
			this.storageService.store('expandSuggestionDocs', false, StorageScope.GLOBAL);
813
			hide(this.details.element);
814
			removeClass(this.element, 'docs-side');
815
			removeClass(this.element, 'docs-below');
816
			this.editor.layoutContentWidget(this);
817
		} else {
818
			this.storageService.store('expandSuggestionDocs', true, StorageScope.GLOBAL);
819 820 821

			this.expandSideOrBelow();

822
			this.showDetails();
823 824 825
		}
	}

826 827
	showDetails(): void {
		if (this.state !== State.Open && this.state !== State.Details) {
J
Joao Moreno 已提交
828 829
			return;
		}
830 831

		show(this.details.element);
832 833 834 835 836 837 838 839 840
		this.renderDetails();

		// With docs showing up, list might need adjustments to keep it close to the cursor
		this.adjustListPosition();

		// with docs showing up widget width/height may change, so reposition the widget
		this.editor.layoutContentWidget(this);

		this.adjustDocsPosition();
J
Joao Moreno 已提交
841

J
Joao Moreno 已提交
842
		this.editor.focus();
J
Joao Moreno 已提交
843 844
	}

845
	private show(): void {
846
		this.updateListHeight();
J
Joao Moreno 已提交
847
		this.suggestWidgetVisible.set(true);
848

J
Joao Moreno 已提交
849
		this.showTimeout = TPromise.timeout(100).then(() => {
J
Joao Moreno 已提交
850
			addClass(this.element, 'visible');
851
			this.onDidShowEmitter.fire(this);
E
Erich Gamma 已提交
852 853 854
		});
	}

855
	private hide(): void {
J
Joao Moreno 已提交
856
		this.suggestWidgetVisible.reset();
S
Sean Kelly 已提交
857
		this.suggestWidgetMultipleSuggestions.reset();
J
Joao Moreno 已提交
858
		removeClass(this.element, 'visible');
E
Erich Gamma 已提交
859 860
	}

861 862 863
	hideWidget(): void {
		clearTimeout(this.loadingTimeout);
		this.setState(State.Hidden);
864
		this.onDidHideEmitter.fire(this);
865 866
	}

J
Joao Moreno 已提交
867
	getPosition(): IContentWidgetPosition {
J
Joao Moreno 已提交
868 869
		if (this.state === State.Hidden) {
			return null;
E
Erich Gamma 已提交
870
		}
J
Joao Moreno 已提交
871 872

		return {
873
			position: this.editor.getPosition(),
A
Alex Dima 已提交
874
			preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE]
J
Joao Moreno 已提交
875
		};
E
Erich Gamma 已提交
876 877
	}

J
Joao Moreno 已提交
878
	getDomNode(): HTMLElement {
E
Erich Gamma 已提交
879 880 881
		return this.element;
	}

J
Joao Moreno 已提交
882
	getId(): string {
E
Erich Gamma 已提交
883 884 885
		return SuggestWidget.ID;
	}

886
	private updateListHeight(): number {
J
Joao Moreno 已提交
887
		let height = 0;
888
		let maxSuggestionsToShow = 11;
E
Erich Gamma 已提交
889

J
Joao Moreno 已提交
890
		if (this.state === State.Empty || this.state === State.Loading) {
891
			height = this.unfocusedHeight;
J
Joao Moreno 已提交
892
		} else {
J
Joao Moreno 已提交
893
			const focus = this.list.getFocusedElements()[0];
894
			const focusHeight = focus ? this.getHeight(focus) : this.unfocusedHeight;
J
Joao Moreno 已提交
895
			height = focusHeight;
J
Joao Moreno 已提交
896

897
			const suggestionCount = (this.list.contentHeight - focusHeight) / this.unfocusedHeight;
898
			height += Math.min(suggestionCount, maxSuggestionsToShow) * this.unfocusedHeight;
J
Joao Moreno 已提交
899
		}
J
Joao Moreno 已提交
900

901
		this.element.style.lineHeight = `${this.unfocusedHeight}px`;
902
		this.listElement.style.height = `${height}px`;
J
Joao Moreno 已提交
903
		this.list.layout(height);
904

E
Erich Gamma 已提交
905 906
		this.editor.layoutContentWidget(this);

J
Joao Moreno 已提交
907
		return height;
J
Joao Moreno 已提交
908 909
	}

910 911 912 913 914 915 916
	private adjustDocsPosition() {
		const cursorCoords = this.editor.getScrolledVisiblePosition(this.editor.getPosition());
		const editorCoords = getDomNodePagePosition(this.editor.getDomNode());
		const cursorX = editorCoords.left + cursorCoords.left;
		const cursorY = editorCoords.top + cursorCoords.top + cursorCoords.height;
		const widgetX = this.element.offsetLeft;
		const widgetY = this.element.offsetTop;
917

918 919 920
		if (widgetX < cursorX - this.listWidth) {
			// Widget is too far to the left of cursor, swap list and docs
			addClass(this.element, 'list-right');
921 922
		} else {
			removeClass(this.element, 'list-right');
923 924
		}

925 926 927 928 929 930 931 932 933
		if (cursorY > widgetY) {
			if (!hasClass(this.element, 'widget-above')) {
				addClass(this.element, 'widget-above');
				// Since the widget was previously not above the cursor,
				// the list needs to be adjusted to keep it close to the cursor
				this.adjustListPosition();
			}
		} else {
			removeClass(this.element, 'widget-above');
934
		}
935 936
	}

937 938 939 940 941 942 943 944 945 946 947
	private expandSideOrBelow() {
		let matches = this.element.style.maxWidth.match(/(\d+)px/);
		if (!matches || Number(matches[1]) < this.maxWidgetWidth) {
			addClass(this.element, 'docs-below');
			removeClass(this.element, 'docs-side');
		} else {
			addClass(this.element, 'docs-side');
			removeClass(this.element, 'docs-below');
		}
	}

948 949 950 951 952 953 954 955 956 957 958
	private adjustListPosition(): void {
		if (hasClass(this.element, 'widget-above')
			&& hasClass(this.element, 'docs-side')
			&& this.details.element.offsetHeight > this.listElement.offsetHeight) {
			// Docs is bigger than list and widget is above cursor, apply margin-top so that list appears right above cursor
			this.listElement.style.marginTop = `${this.details.element.offsetHeight - this.listElement.offsetHeight}px`;
		} else {
			this.listElement.style.marginTop = '0px';
		}
	}

J
Joao Moreno 已提交
959
	private renderDetails(): void {
960
		if (this.state === State.Details || this.state === State.Open) {
J
Joao Moreno 已提交
961
			this.details.render(this.list.getFocusedElements()[0]);
962 963
		} else {
			this.details.render(null);
J
Joao Moreno 已提交
964 965
		}
	}
J
Joao Moreno 已提交
966

967 968 969
	// Heights

	private get focusHeight(): number {
J
Joao Moreno 已提交
970
		return this.unfocusedHeight * 2;
971 972 973
	}

	private get unfocusedHeight(): number {
J
Joao Moreno 已提交
974 975
		const configuration = this.editor.getConfiguration();
		return configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
976 977 978 979 980 981 982 983 984 985 986 987
	}

	// IDelegate

	getHeight(element: ICompletionItem): number {
		return this.unfocusedHeight;
	}

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

J
Joao Moreno 已提交
988
	dispose(): void {
J
Joao Moreno 已提交
989 990 991
		this.state = null;
		this.suggestionSupportsAutoAccept = null;
		this.currentSuggestionDetails = null;
J
Joao Moreno 已提交
992
		this.focusedItem = null;
J
Joao Moreno 已提交
993 994
		this.element = null;
		this.messageElement = null;
J
Joao Moreno 已提交
995
		this.listElement = null;
J
Joao Moreno 已提交
996 997
		this.details.dispose();
		this.details = null;
J
Joao Moreno 已提交
998 999
		this.list.dispose();
		this.list = null;
J
Joao Moreno 已提交
1000
		this.toDispose = dispose(this.toDispose);
A
Alex Dima 已提交
1001 1002 1003 1004
		if (this.loadingTimeout) {
			clearTimeout(this.loadingTimeout);
			this.loadingTimeout = null;
		}
J
Joao Moreno 已提交
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014

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

		if (this.showTimeout) {
			this.showTimeout.cancel();
			this.showTimeout = null;
		}
E
Erich Gamma 已提交
1015
	}
J
Johannes Rieken 已提交
1016
}
1017 1018

registerThemingParticipant((theme, collector) => {
1019
	let matchHighlight = theme.getColor(editorSuggestWidgetHighlightForeground);
1020
	if (matchHighlight) {
1021
		collector.addRule(`.monaco-editor .suggest-widget:not(.frozen) .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`);
1022
	}
1023 1024
	let foreground = theme.getColor(editorSuggestWidgetForeground);
	if (foreground) {
1025
		collector.addRule(`.monaco-editor .suggest-widget { color: ${foreground}; }`);
1026
	}
1027
});