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

6
import 'vs/css!./media/suggest';
J
Johannes Rieken 已提交
7
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
8
import { createMatches } from 'vs/base/common/filters';
9
import * as strings from 'vs/base/common/strings';
M
Matt Bierner 已提交
10
import { Event, Emitter, chain } from 'vs/base/common/event';
11
import { onUnexpectedError } from 'vs/base/common/errors';
J
Joao Moreno 已提交
12
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
13
import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass } from 'vs/base/browser/dom';
J
Joao Moreno 已提交
14
import { IListVirtualDelegate, IListEvent, IListRenderer } from 'vs/base/browser/ui/list/list';
J
Joao Moreno 已提交
15 16
import { List } from 'vs/base/browser/ui/list/listWidget';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
J
Johannes Rieken 已提交
17 18
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
19
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
J
Joao Moreno 已提交
20
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
21 22
import { Context as SuggestContext } from './suggest';
import { ICompletionItem, CompletionModel } from './completionModel';
J
Johannes Rieken 已提交
23
import { alert } from 'vs/base/browser/ui/aria/aria';
J
Joao Moreno 已提交
24
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
25 26
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { IThemeService, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
27
import { registerColor, editorWidgetBackground, listFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder, focusBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry';
B
Benjamin Pasero 已提交
28
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
29
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
30 31
import { IModeService } from 'vs/editor/common/services/modeService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
32 33
import { TimeoutTimer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
34
import { CompletionItemKind, completionKindToCssClass } from 'vs/editor/common/modes';
35
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
36 37 38 39
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { IModelService } from 'vs/editor/common/services/modelService';
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
40
import { FileKind } from 'vs/platform/files/common/files';
E
Erich Gamma 已提交
41

42
const expandSuggestionDocsByDefault = false;
43
const maxSuggestionsToShow = 12;
J
Joao Moreno 已提交
44

E
Erich Gamma 已提交
45 46 47 48
interface ISuggestionTemplateData {
	root: HTMLElement;
	icon: HTMLElement;
	colorspan: HTMLElement;
49
	iconLabel: IconLabel;
50
	typeLabel: HTMLElement;
51
	readMore: HTMLElement;
52
	disposables: IDisposable[];
E
Erich Gamma 已提交
53 54
}

M
Martin Aeschlimann 已提交
55 56 57
/**
 * Suggest widget colors
 */
58
export const editorSuggestWidgetBackground = registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.'));
59
export const editorSuggestWidgetBorder = registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.'));
60 61 62
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.'));
63

M
Martin Aeschlimann 已提交
64

65
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;
66 67 68
function matchesColor(text: string) {
	return text && text.match(colorRegExp) ? text : null;
}
69

70
function canExpandCompletionItem(item: ICompletionItem) {
R
Ramya Achutha Rao 已提交
71 72 73
	if (!item) {
		return false;
	}
74 75 76 77
	const suggestion = item.suggestion;
	if (suggestion.documentation) {
		return true;
	}
78
	return (suggestion.detail && suggestion.detail !== suggestion.label);
79 80
}

J
Joao Moreno 已提交
81
class Renderer implements IListRenderer<ICompletionItem, ISuggestionTemplateData> {
E
Erich Gamma 已提交
82

J
Joao Moreno 已提交
83 84
	constructor(
		private widget: SuggestWidget,
85
		private editor: ICodeEditor,
86 87 88
		private triggerKeybindingLabel: string,
		@IModelService private readonly _modelService: IModelService,
		@IModeService private readonly _modeService: IModeService,
89
		@IThemeService private readonly _themeService: IThemeService,
J
Joao Moreno 已提交
90
	) {
91

J
Joao Moreno 已提交
92
	}
J
Joao Moreno 已提交
93

J
Joao Moreno 已提交
94 95 96
	get templateId(): string {
		return 'suggestion';
	}
E
Erich Gamma 已提交
97

J
Joao Moreno 已提交
98
	renderTemplate(container: HTMLElement): ISuggestionTemplateData {
J
Johannes Rieken 已提交
99
		const data = <ISuggestionTemplateData>Object.create(null);
100
		data.disposables = [];
E
Erich Gamma 已提交
101
		data.root = container;
102
		addClass(data.root, 'show-file-icons');
J
Joao Moreno 已提交
103

J
Joao Moreno 已提交
104 105
		data.icon = append(container, $('.icon'));
		data.colorspan = append(data.icon, $('span.colorspan'));
E
Erich Gamma 已提交
106

J
Joao Moreno 已提交
107
		const text = append(container, $('.contents'));
J
Joao Moreno 已提交
108
		const main = append(text, $('.main'));
109 110 111 112

		data.iconLabel = new IconLabel(main, { supportHighlights: true });
		data.disposables.push(data.iconLabel);

113 114
		data.typeLabel = append(main, $('span.type-label'));

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

118
		const configureFont = () => {
J
Joao Moreno 已提交
119 120 121 122
			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;
123
			const fontWeight = configuration.fontInfo.fontWeight;
J
Johannes Rieken 已提交
124 125
			const fontSizePx = `${fontSize}px`;
			const lineHeightPx = `${lineHeight}px`;
J
Joao Moreno 已提交
126 127

			data.root.style.fontSize = fontSizePx;
128
			data.root.style.fontWeight = fontWeight;
J
Joao Moreno 已提交
129 130 131 132
			main.style.fontFamily = fontFamily;
			main.style.lineHeight = lineHeightPx;
			data.icon.style.height = lineHeightPx;
			data.icon.style.width = lineHeightPx;
133 134
			data.readMore.style.height = lineHeightPx;
			data.readMore.style.width = lineHeightPx;
135 136 137 138
		};

		configureFont();

J
Joao Moreno 已提交
139
		chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
J
Joao Moreno 已提交
140
			.filter(e => e.fontInfo || e.contribInfo)
J
Joao Moreno 已提交
141
			.on(configureFont, null, data.disposables);
142

E
Erich Gamma 已提交
143 144 145
		return data;
	}

146
	renderElement(element: ICompletionItem, _index: number, templateData: ISuggestionTemplateData): void {
J
Johannes Rieken 已提交
147
		const data = <ISuggestionTemplateData>templateData;
148
		const suggestion = (<ICompletionItem>element).suggestion;
E
Erich Gamma 已提交
149

150
		data.icon.className = 'icon ' + completionKindToCssClass(suggestion.kind);
151 152
		data.colorspan.style.backgroundColor = '';

153 154 155 156 157 158

		const labelOptions: IIconLabelValueOptions = {
			labelEscapeNewLines: true,
			matches: createMatches(element.matches)
		};

159 160 161 162 163
		let color: string;
		if (suggestion.kind === CompletionItemKind.Color && (color = matchesColor(suggestion.label) || typeof suggestion.documentation === 'string' && matchesColor(suggestion.documentation))) {
			// special logic for 'color' completion items
			data.icon.className = 'icon customcolor';
			data.colorspan.style.backgroundColor = color;
E
Erich Gamma 已提交
164

165 166
		} else if (suggestion.kind === CompletionItemKind.File && this._themeService.getIconTheme().hasFileIcons) {
			// special logic for 'file' completion items
167 168 169 170 171
			data.icon.className = 'icon hide';
			labelOptions.extraClasses = [].concat(
				getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FILE),
				getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FILE)
			);
172

173 174
		} else if (suggestion.kind === CompletionItemKind.Folder && this._themeService.getIconTheme().hasFolderIcons) {
			// special logic for 'folder' completion items
175
			data.icon.className = 'icon hide';
176
			labelOptions.extraClasses = [].concat(
177 178
				getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.label }), FileKind.FOLDER),
				getIconClasses(this._modelService, this._modeService, URI.from({ scheme: 'fake', path: suggestion.detail }), FileKind.FOLDER)
179
			);
180 181 182 183 184 185
		} else {
			// normal icon
			data.icon.className = 'icon hide';
			labelOptions.extraClasses = [
				`suggest-icon ${completionKindToCssClass(suggestion.kind)}`
			];
186 187 188
		}

		data.iconLabel.setValue(suggestion.label, undefined, labelOptions);
189
		data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, '');
J
Joao Moreno 已提交
190

191
		if (canExpandCompletionItem(element)) {
192 193
			show(data.readMore);
			data.readMore.onmousedown = e => {
194 195 196
				e.stopPropagation();
				e.preventDefault();
			};
197
			data.readMore.onclick = e => {
198 199 200 201 202
				e.stopPropagation();
				e.preventDefault();
				this.widget.toggleDetails();
			};
		} else {
203 204 205
			hide(data.readMore);
			data.readMore.onmousedown = null;
			data.readMore.onclick = null;
206
		}
J
Joao Moreno 已提交
207
	}
J
Joao Moreno 已提交
208

J
Joao Moreno 已提交
209 210
	disposeElement(): void {
		// noop
E
Erich Gamma 已提交
211 212
	}

J
Joao Moreno 已提交
213
	disposeTemplate(templateData: ISuggestionTemplateData): void {
214
		templateData.disposables = dispose(templateData.disposables);
J
Joao Moreno 已提交
215 216
	}
}
E
Erich Gamma 已提交
217

A
Alex Dima 已提交
218
const enum State {
J
Joao Moreno 已提交
219 220 221
	Hidden,
	Loading,
	Empty,
J
Joao Moreno 已提交
222
	Open,
J
Joao Moreno 已提交
223 224 225 226 227 228 229
	Frozen,
	Details
}

class SuggestionDetails {

	private el: HTMLElement;
230
	private close: HTMLElement;
231
	private scrollbar: DomScrollableElement;
232
	private body: HTMLElement;
233
	private header: HTMLElement;
J
Joao Moreno 已提交
234 235
	private type: HTMLElement;
	private docs: HTMLElement;
236 237
	private ariaLabel: string;
	private disposables: IDisposable[];
238
	private renderDisposeable: IDisposable;
239
	private borderWidth: number = 1;
240 241 242 243

	constructor(
		container: HTMLElement,
		private widget: SuggestWidget,
244
		private editor: ICodeEditor,
245
		private markdownRenderer: MarkdownRenderer,
246
		private triggerKeybindingLabel: string
247 248
	) {
		this.disposables = [];
J
Joao Moreno 已提交
249 250

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

253
		this.body = $('.body');
254

255
		this.scrollbar = new DomScrollableElement(this.body, {});
256
		append(this.el, this.scrollbar.getDomNode());
257 258
		this.disposables.push(this.scrollbar);

259 260
		this.header = append(this.body, $('.header'));
		this.close = append(this.header, $('span.close'));
261
		this.close.title = nls.localize('readLess', "Read less...{0}", this.triggerKeybindingLabel);
262
		this.type = append(this.header, $('p.type'));
263

264
		this.docs = append(this.body, $('p.docs'));
265
		this.ariaLabel = null;
266 267 268

		this.configureFont();

J
Joao Moreno 已提交
269 270 271
		chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
			.filter(e => e.fontInfo)
			.on(this.configureFont, this, this.disposables);
272 273

		markdownRenderer.onDidRenderCodeBlock(() => this.scrollbar.scanDomNode(), this, this.disposables);
J
Joao Moreno 已提交
274 275
	}

J
Joao Moreno 已提交
276 277 278
	get element() {
		return this.el;
	}
J
Joao Moreno 已提交
279

280
	render(item: ICompletionItem): void {
281 282
		this.renderDisposeable = dispose(this.renderDisposeable);

283
		if (!item || !canExpandCompletionItem(item)) {
J
Joao Moreno 已提交
284 285
			this.type.textContent = '';
			this.docs.textContent = '';
286
			addClass(this.el, 'no-docs');
287
			this.ariaLabel = null;
J
Joao Moreno 已提交
288 289
			return;
		}
290
		removeClass(this.el, 'no-docs');
291
		if (typeof item.suggestion.documentation === 'string') {
292
			removeClass(this.docs, 'markdown-docs');
293 294
			this.docs.textContent = item.suggestion.documentation;
		} else {
295
			addClass(this.docs, 'markdown-docs');
M
Matt Bierner 已提交
296
			this.docs.innerHTML = '';
297 298 299
			const renderedContents = this.markdownRenderer.render(item.suggestion.documentation);
			this.renderDisposeable = renderedContents;
			this.docs.appendChild(renderedContents.element);
300
		}
301

R
Ramya Achutha Rao 已提交
302 303 304 305 306 307 308 309
		if (item.suggestion.detail) {
			this.type.innerText = item.suggestion.detail;
			show(this.type);
		} else {
			this.type.innerText = '';
			hide(this.type);
		}

310
		this.el.style.height = this.header.offsetHeight + this.docs.offsetHeight + (this.borderWidth * 2) + 'px';
311

312
		this.close.onmousedown = e => {
313 314 315
			e.preventDefault();
			e.stopPropagation();
		};
316
		this.close.onclick = e => {
317 318 319 320
			e.preventDefault();
			e.stopPropagation();
			this.widget.toggleDetails();
		};
321

J
Joao Moreno 已提交
322
		this.body.scrollTop = 0;
323
		this.scrollbar.scanDomNode();
324

325 326 327 328
		this.ariaLabel = strings.format(
			'{0}{1}',
			item.suggestion.detail || '',
			item.suggestion.documentation ? (typeof item.suggestion.documentation === 'string' ? item.suggestion.documentation : item.suggestion.documentation.value) : '');
329 330 331 332
	}

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

335 336 337 338 339 340 341 342
	scrollDown(much = 8): void {
		this.body.scrollTop += much;
	}

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

343 344 345 346 347 348 349 350
	scrollTop(): void {
		this.body.scrollTop = 0;
	}

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

351 352 353 354 355 356 357 358
	pageDown(): void {
		this.scrollDown(80);
	}

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

359 360 361 362
	setBorderWidth(width: number): void {
		this.borderWidth = width;
	}

363
	private configureFont() {
J
Joao Moreno 已提交
364 365 366
		const configuration = this.editor.getConfiguration();
		const fontFamily = configuration.fontInfo.fontFamily;
		const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize;
367
		const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
368
		const fontWeight = configuration.fontInfo.fontWeight;
J
Johannes Rieken 已提交
369
		const fontSizePx = `${fontSize}px`;
370
		const lineHeightPx = `${lineHeight}px`;
J
Joao Moreno 已提交
371

J
Joao Moreno 已提交
372
		this.el.style.fontSize = fontSizePx;
373
		this.el.style.fontWeight = fontWeight;
J
Joao Moreno 已提交
374
		this.type.style.fontFamily = fontFamily;
375 376
		this.close.style.height = lineHeightPx;
		this.close.style.width = lineHeightPx;
377
	}
378

379 380
	dispose(): void {
		this.disposables = dispose(this.disposables);
381
		this.renderDisposeable = dispose(this.renderDisposeable);
J
Joao Moreno 已提交
382
	}
J
Joao Moreno 已提交
383 384
}

385 386 387 388 389 390
export interface ISelectedSuggestion {
	item: ICompletionItem;
	index: number;
	model: CompletionModel;
}

J
Joao Moreno 已提交
391
export class SuggestWidget implements IContentWidget, IListVirtualDelegate<ICompletionItem>, IDisposable {
J
Joao Moreno 已提交
392

M
Matt Bierner 已提交
393
	private static readonly ID: string = 'editor.widget.suggestWidget';
E
Erich Gamma 已提交
394

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

J
Joao Moreno 已提交
398
	// Editor.IContentWidget.allowEditorOverflow
399
	readonly allowEditorOverflow = true;
J
Joao Moreno 已提交
400

J
Joao Moreno 已提交
401
	private state: State;
E
Erich Gamma 已提交
402
	private isAuto: boolean;
403
	private loadingTimeout: any;
404
	private currentSuggestionDetails: CancelablePromise<void>;
405
	private focusedItem: ICompletionItem;
406
	private ignoreFocusEvents = false;
J
Joao Moreno 已提交
407
	private completionModel: CompletionModel;
J
Joao Moreno 已提交
408

E
Erich Gamma 已提交
409
	private element: HTMLElement;
J
Joao Moreno 已提交
410
	private messageElement: HTMLElement;
J
Joao Moreno 已提交
411
	private listElement: HTMLElement;
J
Joao Moreno 已提交
412
	private details: SuggestionDetails;
413
	private list: List<ICompletionItem>;
414
	private listHeight: number;
E
Erich Gamma 已提交
415

A
Alex Dima 已提交
416 417
	private suggestWidgetVisible: IContextKey<boolean>;
	private suggestWidgetMultipleSuggestions: IContextKey<boolean>;
J
Joao Moreno 已提交
418

419 420
	private readonly editorBlurTimeout = new TimeoutTimer();
	private readonly showTimeout = new TimeoutTimer();
J
Joao Moreno 已提交
421
	private toDispose: IDisposable[];
J
Joao Moreno 已提交
422

423 424
	private onDidSelectEmitter = new Emitter<ISelectedSuggestion>();
	private onDidFocusEmitter = new Emitter<ISelectedSuggestion>();
425 426 427
	private onDidHideEmitter = new Emitter<this>();
	private onDidShowEmitter = new Emitter<this>();

428

429 430
	readonly onDidSelect: Event<ISelectedSuggestion> = this.onDidSelectEmitter.event;
	readonly onDidFocus: Event<ISelectedSuggestion> = this.onDidFocusEmitter.event;
431 432
	readonly onDidHide: Event<this> = this.onDidHideEmitter.event;
	readonly onDidShow: Event<this> = this.onDidShowEmitter.event;
433

434
	private readonly maxWidgetWidth = 660;
435
	private readonly listWidth = 330;
436
	private storageService: IStorageService;
437 438
	private detailsFocusBorderColor: string;
	private detailsBorderColor: string;
439

440 441
	private firstFocusInCurrentList: boolean = false;

442
	constructor(
A
Alex Dima 已提交
443
		private editor: ICodeEditor,
J
Joao Moreno 已提交
444
		@ITelemetryService private telemetryService: ITelemetryService,
445
		@IContextKeyService contextKeyService: IContextKeyService,
446
		@IThemeService themeService: IThemeService,
447
		@IStorageService storageService: IStorageService,
448 449
		@IKeybindingService keybindingService: IKeybindingService,
		@IModeService modeService: IModeService,
450 451
		@IOpenerService openerService: IOpenerService,
		@IInstantiationService instantiationService: IInstantiationService,
452
	) {
453 454
		const kb = keybindingService.lookupKeybinding('editor.action.triggerSuggest');
		const triggerKeybindingLabel = !kb ? '' : ` (${kb.getLabel()})`;
455
		const markdownRenderer = new MarkdownRenderer(editor, modeService, openerService);
456

E
Erich Gamma 已提交
457
		this.isAuto = false;
J
Joao Moreno 已提交
458
		this.focusedItem = null;
459
		this.storageService = storageService;
E
Erich Gamma 已提交
460

461
		this.element = $('.editor-widget.suggest-widget');
A
Alex Dima 已提交
462
		if (!this.editor.getConfiguration().contribInfo.iconsInSuggestions) {
J
Joao Moreno 已提交
463
			addClass(this.element, 'no-icons');
E
Erich Gamma 已提交
464 465
		}

J
Joao Moreno 已提交
466
		this.messageElement = append(this.element, $('.message'));
J
Joao Moreno 已提交
467
		this.listElement = append(this.element, $('.tree'));
468
		this.details = new SuggestionDetails(this.element, this, this.editor, markdownRenderer, triggerKeybindingLabel);
J
Joao Moreno 已提交
469

470
		let renderer = instantiationService.createInstance(Renderer, this, this.editor, triggerKeybindingLabel);
J
Joao Moreno 已提交
471

472 473
		this.list = new List(this.listElement, this, [renderer], {
			useShadows: false,
J
Joao Moreno 已提交
474
			selectOnMouseDown: true,
J
Joao Moreno 已提交
475 476
			focusOnMouseDown: false,
			openController: { shouldOpen: () => false }
477
		});
J
Joao Moreno 已提交
478 479

		this.toDispose = [
480
			attachListStyler(this.list, themeService, {
481
				listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,
482 483
				listInactiveFocusOutline: activeContrastBorder
			}),
M
Martin Aeschlimann 已提交
484
			themeService.onThemeChange(t => this.onThemeChange(t)),
485
			editor.onDidLayoutChange(() => this.onEditorLayoutChange()),
J
Joao Moreno 已提交
486
			this.list.onSelectionChange(e => this.onListSelection(e)),
J
Joao Moreno 已提交
487
			this.list.onFocusChange(e => this.onListFocus(e)),
488
			this.editor.onDidChangeCursorSelection(() => this.onCursorSelectionChanged())
J
Joao Moreno 已提交
489 490
		];

491 492
		this.suggestWidgetVisible = SuggestContext.Visible.bindTo(contextKeyService);
		this.suggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(contextKeyService);
J
Joao Moreno 已提交
493

J
Joao Moreno 已提交
494 495
		this.editor.addContentWidget(this);
		this.setState(State.Hidden);
496

M
Martin Aeschlimann 已提交
497
		this.onThemeChange(themeService.getTheme());
J
Joao Moreno 已提交
498
	}
E
Erich Gamma 已提交
499

J
Joao Moreno 已提交
500 501 502 503
	private onCursorSelectionChanged(): void {
		if (this.state === State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
504

J
Joao Moreno 已提交
505 506
		this.editor.layoutContentWidget(this);
	}
J
Joao Moreno 已提交
507

508 509 510 511 512 513
	private onEditorLayoutChange(): void {
		if ((this.state === State.Open || this.state === State.Details) && this.expandDocsSettingFromStorage()) {
			this.expandSideOrBelow();
		}
	}

J
Joao Moreno 已提交
514
	private onListSelection(e: IListEvent<ICompletionItem>): void {
J
Joao Moreno 已提交
515 516 517
		if (!e.elements.length) {
			return;
		}
E
Erich Gamma 已提交
518

J
Joao Moreno 已提交
519
		const item = e.elements[0];
520
		const index = e.indexes[0];
521
		item.resolve(CancellationToken.None).then(() => {
522
			this.onDidSelectEmitter.fire({ item, index, model: this.completionModel });
J
Johannes Rieken 已提交
523 524
			alert(nls.localize('suggestionAriaAccepted', "{0}, accepted", item.suggestion.label));
			this.editor.focus();
525
		});
J
Joao Moreno 已提交
526
	}
E
Erich Gamma 已提交
527

J
Johannes Rieken 已提交
528
	private _getSuggestionAriaAlertLabel(item: ICompletionItem): string {
529
		const isSnippet = item.suggestion.kind === CompletionItemKind.Snippet;
530 531 532 533 534 535 536

		if (!canExpandCompletionItem(item)) {
			return isSnippet ? nls.localize('ariaCurrentSnippetSuggestion', "{0}, snippet suggestion", item.suggestion.label)
				: nls.localize('ariaCurrentSuggestion', "{0}, suggestion", item.suggestion.label);
		} else if (this.expandDocsSettingFromStorage()) {
			return isSnippet ? nls.localize('ariaCurrentSnippeSuggestionReadDetails', "{0}, snippet suggestion. Reading details. {1}", item.suggestion.label, this.details.getAriaLabel())
				: nls.localize('ariaCurrenttSuggestionReadDetails', "{0}, suggestion. Reading details. {1}", item.suggestion.label, this.details.getAriaLabel());
537
		} else {
538 539
			return isSnippet ? nls.localize('ariaCurrentSnippetSuggestionWithDetails', "{0}, snippet suggestion, has details", item.suggestion.label)
				: nls.localize('ariaCurrentSuggestionWithDetails', "{0}, suggestion, has details", item.suggestion.label);
540 541 542 543
		}
	}

	private _lastAriaAlertLabel: string;
J
Johannes Rieken 已提交
544
	private _ariaAlert(newAriaAlertLabel: string): void {
545 546 547 548 549 550 551 552 553
		if (this._lastAriaAlertLabel === newAriaAlertLabel) {
			return;
		}
		this._lastAriaAlertLabel = newAriaAlertLabel;
		if (this._lastAriaAlertLabel) {
			alert(this._lastAriaAlertLabel);
		}
	}

M
Martin Aeschlimann 已提交
554
	private onThemeChange(theme: ITheme) {
M
Matt Bierner 已提交
555
		const backgroundColor = theme.getColor(editorSuggestWidgetBackground);
M
Martin Aeschlimann 已提交
556
		if (backgroundColor) {
557 558
			this.listElement.style.backgroundColor = backgroundColor.toString();
			this.details.element.style.backgroundColor = backgroundColor.toString();
R
Ramya Achutha Rao 已提交
559
			this.messageElement.style.backgroundColor = backgroundColor.toString();
M
Martin Aeschlimann 已提交
560
		}
M
Matt Bierner 已提交
561
		const borderColor = theme.getColor(editorSuggestWidgetBorder);
M
Martin Aeschlimann 已提交
562
		if (borderColor) {
563 564 565
			this.listElement.style.borderColor = borderColor.toString();
			this.details.element.style.borderColor = borderColor.toString();
			this.messageElement.style.borderColor = borderColor.toString();
566
			this.detailsBorderColor = borderColor.toString();
M
Martin Aeschlimann 已提交
567
		}
M
Matt Bierner 已提交
568
		const focusBorderColor = theme.getColor(focusBorder);
569 570 571
		if (focusBorderColor) {
			this.detailsFocusBorderColor = focusBorderColor.toString();
		}
572
		this.details.setBorderWidth(theme.type === 'hc' ? 2 : 1);
M
Martin Aeschlimann 已提交
573 574
	}

J
Joao Moreno 已提交
575
	private onListFocus(e: IListEvent<ICompletionItem>): void {
576 577 578 579
		if (this.ignoreFocusEvents) {
			return;
		}

J
Joao Moreno 已提交
580
		if (!e.elements.length) {
J
Joao Moreno 已提交
581 582 583 584 585
			if (this.currentSuggestionDetails) {
				this.currentSuggestionDetails.cancel();
				this.currentSuggestionDetails = null;
				this.focusedItem = null;
			}
586

J
Joao Moreno 已提交
587
			this._ariaAlert(null);
J
Joao Moreno 已提交
588 589
			return;
		}
E
Erich Gamma 已提交
590

J
Joao Moreno 已提交
591
		const item = e.elements[0];
J
Johannes Rieken 已提交
592
		const index = e.indexes[0];
593

594
		this.firstFocusInCurrentList = !this.focusedItem;
J
Johannes Rieken 已提交
595
		if (item !== this.focusedItem) {
E
Erich Gamma 已提交
596

J
Joao Moreno 已提交
597

J
Johannes Rieken 已提交
598 599 600 601
			if (this.currentSuggestionDetails) {
				this.currentSuggestionDetails.cancel();
				this.currentSuggestionDetails = null;
			}
E
Erich Gamma 已提交
602

J
Johannes Rieken 已提交
603
			this.focusedItem = item;
J
Joao Moreno 已提交
604

J
Johannes Rieken 已提交
605
			this.list.reveal(index);
606

J
Johannes Rieken 已提交
607
			this.currentSuggestionDetails = createCancelablePromise(token => item.resolve(token));
608

J
Johannes Rieken 已提交
609 610 611 612
			this.currentSuggestionDetails.then(() => {
				if (this.list.length < index) {
					return;
				}
613

J
Johannes Rieken 已提交
614 615 616 617 618
				// item can have extra information, so re-render
				this.ignoreFocusEvents = true;
				this.list.splice(index, 1, [item]);
				this.list.setFocus([index]);
				this.ignoreFocusEvents = false;
619

J
Johannes Rieken 已提交
620 621 622 623 624 625 626 627 628 629 630 631 632
				if (this.expandDocsSettingFromStorage()) {
					this.showDetails();
				} else {
					removeClass(this.element, 'docs-side');
				}

				this._ariaAlert(this._getSuggestionAriaAlertLabel(item));
			}).catch(onUnexpectedError).then(() => {
				if (this.focusedItem === item) {
					this.currentSuggestionDetails = null;
				}
			});
		}
633 634

		// emit an event
635
		this.onDidFocusEmitter.fire({ item, index, model: this.completionModel });
J
Joao Moreno 已提交
636
	}
J
Joao Moreno 已提交
637 638

	private setState(state: State): void {
J
Joao Moreno 已提交
639 640 641 642
		if (!this.element) {
			return;
		}

643
		const stateChanged = this.state !== state;
J
Joao Moreno 已提交
644 645
		this.state = state;

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

J
Joao Moreno 已提交
648 649
		switch (state) {
			case State.Hidden:
650
				hide(this.messageElement, this.details.element, this.listElement);
J
Joao Moreno 已提交
651
				this.hide();
652
				this.listHeight = 0;
A
tslint  
Alex Dima 已提交
653
				if (stateChanged) {
654
					this.list.splice(0, this.list.length);
A
tslint  
Alex Dima 已提交
655
				}
656
				this.focusedItem = null;
657
				break;
J
Joao Moreno 已提交
658
			case State.Loading:
J
Joao Moreno 已提交
659
				this.messageElement.textContent = SuggestWidget.LOADING_MESSAGE;
J
Joao Moreno 已提交
660
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
661
				show(this.messageElement);
R
Ramya Achutha Rao 已提交
662
				removeClass(this.element, 'docs-side');
663
				this.show();
664
				this.focusedItem = null;
J
Joao Moreno 已提交
665 666
				break;
			case State.Empty:
J
Joao Moreno 已提交
667
				this.messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE;
J
Joao Moreno 已提交
668
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
669
				show(this.messageElement);
R
Ramya Achutha Rao 已提交
670
				removeClass(this.element, 'docs-side');
671
				this.show();
672
				this.focusedItem = null;
J
Joao Moreno 已提交
673 674
				break;
			case State.Open:
675
				hide(this.messageElement);
676
				show(this.listElement);
677
				this.show();
J
Joao Moreno 已提交
678 679
				break;
			case State.Frozen:
680
				hide(this.messageElement);
J
Joao Moreno 已提交
681
				show(this.listElement);
682
				this.show();
J
Joao Moreno 已提交
683 684
				break;
			case State.Details:
685 686
				hide(this.messageElement);
				show(this.details.element, this.listElement);
687
				this.show();
688
				this._ariaAlert(this.details.getAriaLabel());
J
Joao Moreno 已提交
689 690
				break;
		}
691 692
	}

693
	showTriggered(auto: boolean, delay: number) {
J
Joao Moreno 已提交
694 695 696
		if (this.state !== State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
697

698
		this.isAuto = !!auto;
J
Joao Moreno 已提交
699 700 701 702 703

		if (!this.isAuto) {
			this.loadingTimeout = setTimeout(() => {
				this.loadingTimeout = null;
				this.setState(State.Loading);
704
			}, delay);
J
Joao Moreno 已提交
705
		}
706
	}
E
Erich Gamma 已提交
707

708
	showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean): void {
A
Alex Dima 已提交
709 710 711 712
		if (this.loadingTimeout) {
			clearTimeout(this.loadingTimeout);
			this.loadingTimeout = null;
		}
J
Joao Moreno 已提交
713

J
Johannes Rieken 已提交
714 715 716 717 718
		if (this.currentSuggestionDetails) {
			this.currentSuggestionDetails.cancel();
			this.currentSuggestionDetails = null;
		}

719 720 721
		if (this.completionModel !== completionModel) {
			this.completionModel = completionModel;
		}
J
Joao Moreno 已提交
722

R
Ramya Achutha Rao 已提交
723
		if (isFrozen && this.state !== State.Empty && this.state !== State.Hidden) {
724 725
			this.setState(State.Frozen);
			return;
726 727
		}

728 729
		let visibleCount = this.completionModel.items.length;

J
Joao Moreno 已提交
730
		const isEmpty = visibleCount === 0;
731
		this.suggestWidgetMultipleSuggestions.set(visibleCount > 1);
J
Joao Moreno 已提交
732 733

		if (isEmpty) {
734
			if (isAuto) {
J
Joao Moreno 已提交
735 736
				this.setState(State.Hidden);
			} else {
Y
Yuki Ueda 已提交
737
				this.setState(State.Empty);
J
Joao Moreno 已提交
738 739
			}

J
Joao Moreno 已提交
740
			this.completionModel = null;
J
Joao Moreno 已提交
741

J
Joao Moreno 已提交
742
		} else {
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757

			if (this.state !== State.Open) {
				const { stats } = this.completionModel;
				stats['wasAutomaticallyTriggered'] = !!isAuto;
				/* __GDPR__
					"suggestWidget" : {
						"wasAutomaticallyTriggered" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
						"${include}": [
							"${ICompletionStats}",
							"${EditorTelemetryData}"
						]
					}
				*/
				this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() });
			}
J
Joao Moreno 已提交
758

J
Johannes Rieken 已提交
759
			this.focusedItem = null;
760 761
			this.list.splice(0, this.list.length, this.completionModel.items);

R
Ramya Achutha Rao 已提交
762 763 764 765 766
			if (isFrozen) {
				this.setState(State.Frozen);
			} else {
				this.setState(State.Open);
			}
R
Ramya Achutha Rao 已提交
767

J
Johannes Rieken 已提交
768
			this.list.reveal(selectionIndex, 0);
J
Joao Moreno 已提交
769 770
			this.list.setFocus([selectionIndex]);

R
Ramya Achutha Rao 已提交
771 772 773 774
			// Reset focus border
			if (this.detailsBorderColor) {
				this.details.element.style.borderColor = this.detailsBorderColor;
			}
J
Joao Moreno 已提交
775
		}
776
	}
E
Erich Gamma 已提交
777

J
Joao Moreno 已提交
778
	selectNextPage(): boolean {
J
Joao Moreno 已提交
779 780 781 782 783 784 785 786 787 788 789 790
		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 已提交
791 792
	}

J
Joao Moreno 已提交
793
	selectNext(): boolean {
J
Joao Moreno 已提交
794 795 796 797 798 799 800 801 802
		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 已提交
803 804
	}

805 806 807 808 809
	selectLast(): boolean {
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
810
				this.details.scrollBottom();
811 812 813 814 815 816 817 818 819
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusLast();
				return true;
		}
	}

J
Joao Moreno 已提交
820
	selectPreviousPage(): boolean {
J
Joao Moreno 已提交
821 822 823 824 825 826 827 828 829 830 831 832
		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 已提交
833 834
	}

J
Joao Moreno 已提交
835
	selectPrevious(): boolean {
J
Joao Moreno 已提交
836 837 838 839 840 841 842
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusPrevious(1, true);
J
Joao Moreno 已提交
843
				return false;
J
Joao Moreno 已提交
844
		}
E
Erich Gamma 已提交
845 846
	}

847 848 849 850 851
	selectFirst(): boolean {
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
852
				this.details.scrollTop();
853 854 855 856 857 858 859 860 861
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusFirst();
				return true;
		}
	}

862
	getFocusedItem(): ISelectedSuggestion {
863 864 865 866
		if (this.state !== State.Hidden
			&& this.state !== State.Empty
			&& this.state !== State.Loading) {

867 868 869 870 871
			return {
				item: this.list.getFocusedElements()[0],
				index: this.list.getFocus()[0],
				model: this.completionModel
			};
J
Joao Moreno 已提交
872
		}
873
		return undefined;
E
Erich Gamma 已提交
874 875
	}

876
	toggleDetailsFocus(): void {
J
Joao Moreno 已提交
877 878
		if (this.state === State.Details) {
			this.setState(State.Open);
879 880 881
			if (this.detailsBorderColor) {
				this.details.element.style.borderColor = this.detailsBorderColor;
			}
882
		} else if (this.state === State.Open && this.expandDocsSettingFromStorage()) {
883
			this.setState(State.Details);
884 885 886
			if (this.detailsFocusBorderColor) {
				this.details.element.style.borderColor = this.detailsFocusBorderColor;
			}
J
Joao Moreno 已提交
887
		}
K
kieferrm 已提交
888
		/* __GDPR__
K
kieferrm 已提交
889 890 891 892 893 894
			"suggestWidget:toggleDetailsFocus" : {
				"${include}": [
					"${EditorTelemetryData}"
				]
			}
		*/
895
		this.telemetryService.publicLog('suggestWidget:toggleDetailsFocus', this.editor.getTelemetryData());
896
	}
J
Joao Moreno 已提交
897

898
	toggleDetails(): void {
R
Ramya Achutha Rao 已提交
899 900 901
		if (!canExpandCompletionItem(this.list.getFocusedElements()[0])) {
			return;
		}
902

903 904
		if (this.expandDocsSettingFromStorage()) {
			this.updateExpandDocsSetting(false);
905
			hide(this.details.element);
906
			removeClass(this.element, 'docs-side');
907
			removeClass(this.element, 'docs-below');
908
			this.editor.layoutContentWidget(this);
K
kieferrm 已提交
909
			/* __GDPR__
K
kieferrm 已提交
910 911 912 913 914 915
				"suggestWidget:collapseDetails" : {
					"${include}": [
						"${EditorTelemetryData}"
					]
				}
			*/
916
			this.telemetryService.publicLog('suggestWidget:collapseDetails', this.editor.getTelemetryData());
917
		} else {
918
			if (this.state !== State.Open && this.state !== State.Details && this.state !== State.Frozen) {
919 920 921
				return;
			}

922
			this.updateExpandDocsSetting(true);
923
			this.showDetails();
924
			this._ariaAlert(this.details.getAriaLabel());
K
kieferrm 已提交
925
			/* __GDPR__
K
kieferrm 已提交
926 927 928 929 930 931
				"suggestWidget:expandDetails" : {
					"${include}": [
						"${EditorTelemetryData}"
					]
				}
			*/
932
			this.telemetryService.publicLog('suggestWidget:expandDetails', this.editor.getTelemetryData());
933
		}
934

935 936
	}

937
	showDetails(): void {
R
Ramya Achutha Rao 已提交
938
		this.expandSideOrBelow();
939 940

		show(this.details.element);
941
		this.details.render(this.list.getFocusedElements()[0]);
942
		this.details.element.style.maxHeight = this.maxWidgetHeight + 'px';
943

R
Ramya Achutha Rao 已提交
944 945
		// Reset margin-top that was set as Fix for #26416
		this.listElement.style.marginTop = '0px';
946 947 948 949 950

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

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

J
Joao Moreno 已提交
952
		this.editor.focus();
J
Joao Moreno 已提交
953 954
	}

955
	private show(): void {
956 957 958 959 960 961
		const newHeight = this.updateListHeight();
		if (newHeight !== this.listHeight) {
			this.editor.layoutContentWidget(this);
			this.listHeight = newHeight;
		}

J
Joao Moreno 已提交
962
		this.suggestWidgetVisible.set(true);
963

964
		this.showTimeout.cancelAndSet(() => {
J
Joao Moreno 已提交
965
			addClass(this.element, 'visible');
966
			this.onDidShowEmitter.fire(this);
967
		}, 100);
E
Erich Gamma 已提交
968 969
	}

970
	private hide(): void {
J
Joao Moreno 已提交
971
		this.suggestWidgetVisible.reset();
S
Sean Kelly 已提交
972
		this.suggestWidgetMultipleSuggestions.reset();
J
Joao Moreno 已提交
973
		removeClass(this.element, 'visible');
E
Erich Gamma 已提交
974 975
	}

976 977 978
	hideWidget(): void {
		clearTimeout(this.loadingTimeout);
		this.setState(State.Hidden);
979
		this.onDidHideEmitter.fire(this);
980 981
	}

J
Joao Moreno 已提交
982
	getPosition(): IContentWidgetPosition {
J
Joao Moreno 已提交
983 984
		if (this.state === State.Hidden) {
			return null;
E
Erich Gamma 已提交
985
		}
J
Joao Moreno 已提交
986 987

		return {
988
			position: this.editor.getPosition(),
A
Alex Dima 已提交
989
			preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE]
J
Joao Moreno 已提交
990
		};
E
Erich Gamma 已提交
991 992
	}

J
Joao Moreno 已提交
993
	getDomNode(): HTMLElement {
E
Erich Gamma 已提交
994 995 996
		return this.element;
	}

J
Joao Moreno 已提交
997
	getId(): string {
E
Erich Gamma 已提交
998 999 1000
		return SuggestWidget.ID;
	}

1001
	private updateListHeight(): number {
J
Joao Moreno 已提交
1002
		let height = 0;
E
Erich Gamma 已提交
1003

J
Joao Moreno 已提交
1004
		if (this.state === State.Empty || this.state === State.Loading) {
1005
			height = this.unfocusedHeight;
J
Joao Moreno 已提交
1006
		} else {
1007 1008
			const suggestionCount = this.list.contentHeight / this.unfocusedHeight;
			height = Math.min(suggestionCount, maxSuggestionsToShow) * this.unfocusedHeight;
J
Joao Moreno 已提交
1009
		}
J
Joao Moreno 已提交
1010

1011
		this.element.style.lineHeight = `${this.unfocusedHeight}px`;
1012
		this.listElement.style.height = `${height}px`;
J
Joao Moreno 已提交
1013
		this.list.layout(height);
1014
		return height;
J
Joao Moreno 已提交
1015 1016
	}

R
Ramya Achutha Rao 已提交
1017 1018 1019
	/**
	 * Adds the propert classes, margins when positioning the docs to the side
	 */
1020
	private adjustDocsPosition() {
R
Ramya Achutha Rao 已提交
1021
		const lineHeight = this.editor.getConfiguration().fontInfo.lineHeight;
1022 1023 1024 1025
		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;
1026 1027 1028
		const widgetCoords = getDomNodePagePosition(this.element);
		const widgetX = widgetCoords.left;
		const widgetY = widgetCoords.top;
1029

1030 1031 1032
		if (widgetX < cursorX - this.listWidth) {
			// Widget is too far to the left of cursor, swap list and docs
			addClass(this.element, 'list-right');
1033 1034
		} else {
			removeClass(this.element, 'list-right');
1035 1036
		}

R
Ramya Achutha Rao 已提交
1037 1038 1039
		// Compare top of the cursor (cursorY - lineheight) with widgetTop to determine if
		// margin-top needs to be applied on list to make it appear right above the cursor
		// Cannot compare cursorY directly as it may be a few decimals off due to zoooming
R
Ramya Achutha Rao 已提交
1040
		if (hasClass(this.element, 'docs-side')
R
Ramya Achutha Rao 已提交
1041
			&& cursorY - lineHeight > widgetY
R
Ramya Achutha Rao 已提交
1042 1043 1044 1045 1046
			&& this.details.element.offsetHeight > this.listElement.offsetHeight) {

			// Fix for #26416
			// 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`;
1047
		}
1048 1049
	}

R
Ramya Achutha Rao 已提交
1050 1051 1052
	/**
	 * Adds the proper classes for positioning the docs to the side or below
	 */
1053
	private expandSideOrBelow() {
1054 1055 1056 1057 1058 1059
		if (!canExpandCompletionItem(this.focusedItem) && this.firstFocusInCurrentList) {
			removeClass(this.element, 'docs-side');
			removeClass(this.element, 'docs-below');
			return;
		}

1060 1061 1062 1063
		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');
1064
		} else if (canExpandCompletionItem(this.focusedItem)) {
1065 1066 1067 1068 1069
			addClass(this.element, 'docs-side');
			removeClass(this.element, 'docs-below');
		}
	}

1070 1071
	// Heights

1072 1073
	private get maxWidgetHeight(): number {
		return this.unfocusedHeight * maxSuggestionsToShow;
1074 1075 1076
	}

	private get unfocusedHeight(): number {
J
Joao Moreno 已提交
1077 1078
		const configuration = this.editor.getConfiguration();
		return configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
	}

	// IDelegate

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

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

1091
	private expandDocsSettingFromStorage(): boolean {
1092
		return this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault);
1093 1094 1095
	}

	private updateExpandDocsSetting(value: boolean) {
1096
		this.storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL);
1097 1098
	}

J
Joao Moreno 已提交
1099
	dispose(): void {
J
Joao Moreno 已提交
1100 1101
		this.state = null;
		this.currentSuggestionDetails = null;
J
Joao Moreno 已提交
1102
		this.focusedItem = null;
J
Joao Moreno 已提交
1103 1104
		this.element = null;
		this.messageElement = null;
J
Joao Moreno 已提交
1105
		this.listElement = null;
J
Joao Moreno 已提交
1106 1107
		this.details.dispose();
		this.details = null;
J
Joao Moreno 已提交
1108 1109
		this.list.dispose();
		this.list = null;
J
Joao Moreno 已提交
1110
		this.toDispose = dispose(this.toDispose);
A
Alex Dima 已提交
1111 1112 1113 1114
		if (this.loadingTimeout) {
			clearTimeout(this.loadingTimeout);
			this.loadingTimeout = null;
		}
J
Joao Moreno 已提交
1115

1116 1117
		this.editorBlurTimeout.dispose();
		this.showTimeout.dispose();
E
Erich Gamma 已提交
1118
	}
J
Johannes Rieken 已提交
1119
}
1120 1121

registerThemingParticipant((theme, collector) => {
M
Matt Bierner 已提交
1122
	const matchHighlight = theme.getColor(editorSuggestWidgetHighlightForeground);
1123
	if (matchHighlight) {
J
Johannes Rieken 已提交
1124
		collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`);
1125
	}
M
Matt Bierner 已提交
1126
	const foreground = theme.getColor(editorSuggestWidgetForeground);
1127
	if (foreground) {
1128
		collector.addRule(`.monaco-editor .suggest-widget { color: ${foreground}; }`);
1129
	}
M
Matt Bierner 已提交
1130 1131 1132 1133 1134

	const link = theme.getColor(textLinkForeground);
	if (link) {
		collector.addRule(`.monaco-editor .suggest-widget a { color: ${link}; }`);
	}
1135

M
Matt Bierner 已提交
1136
	const codeBackground = theme.getColor(textCodeBlockBackground);
1137 1138 1139
	if (codeBackground) {
		collector.addRule(`.monaco-editor .suggest-widget code { background-color: ${codeBackground}; }`);
	}
1140
});