suggestWidget.ts 37.9 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';
J
Joao Moreno 已提交
10
import { Event, Emitter } from 'vs/base/common/event';
11
import { onUnexpectedError } from 'vs/base/common/errors';
12
import { IDisposable, dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
13
import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass, addDisposableListener } from 'vs/base/browser/dom';
J
Joao Moreno 已提交
14
import { IListVirtualDelegate, IListEvent, IListRenderer, IListMouseEvent } 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, CompletionItem } from './suggest';
import { 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
import { TimeoutTimer, CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async';
33
import { CompletionItemKind, completionKindToCssClass } from 'vs/editor/common/modes';
34
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
35 36 37 38
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';
39
import { FileKind } from 'vs/platform/files/common/files';
40
import { MarkdownString } from 'vs/base/common/htmlContent';
E
Erich Gamma 已提交
41

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

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

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

M
Martin Aeschlimann 已提交
63

64
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;
J
Johannes Rieken 已提交
65 66 67 68 69 70 71 72 73 74
function extractColor(item: CompletionItem, out: string[]): boolean {
	if (item.completion.label.match(colorRegExp)) {
		out[0] = item.completion.label;
		return true;
	}
	if (typeof item.completion.documentation === 'string' && item.completion.documentation.match(colorRegExp)) {
		out[0] = item.completion.documentation;
		return true;
	}
	return false;
75
}
76

M
Matt Bierner 已提交
77
function canExpandCompletionItem(item: CompletionItem | null) {
R
Ramya Achutha Rao 已提交
78 79 80
	if (!item) {
		return false;
	}
81
	const suggestion = item.completion;
82 83 84
	if (suggestion.documentation) {
		return true;
	}
85
	return (suggestion.detail && suggestion.detail !== suggestion.label);
86 87
}

88
class Renderer implements IListRenderer<CompletionItem, ISuggestionTemplateData> {
E
Erich Gamma 已提交
89

J
Joao Moreno 已提交
90 91
	constructor(
		private widget: SuggestWidget,
92
		private editor: ICodeEditor,
93 94 95
		private triggerKeybindingLabel: string,
		@IModelService private readonly _modelService: IModelService,
		@IModeService private readonly _modeService: IModeService,
96
		@IThemeService private readonly _themeService: IThemeService,
J
Joao Moreno 已提交
97
	) {
98

J
Joao Moreno 已提交
99
	}
J
Joao Moreno 已提交
100

J
Joao Moreno 已提交
101 102 103
	get templateId(): string {
		return 'suggestion';
	}
E
Erich Gamma 已提交
104

J
Joao Moreno 已提交
105
	renderTemplate(container: HTMLElement): ISuggestionTemplateData {
J
Johannes Rieken 已提交
106
		const data = <ISuggestionTemplateData>Object.create(null);
107
		data.disposables = new DisposableStore();
108

E
Erich Gamma 已提交
109
		data.root = container;
110
		addClass(data.root, 'show-file-icons');
J
Joao Moreno 已提交
111

J
Joao Moreno 已提交
112 113
		data.icon = append(container, $('.icon'));
		data.colorspan = append(data.icon, $('span.colorspan'));
E
Erich Gamma 已提交
114

J
Joao Moreno 已提交
115
		const text = append(container, $('.contents'));
J
Joao Moreno 已提交
116
		const main = append(text, $('.main'));
117

118
		data.iconLabel = new IconLabel(main, { supportHighlights: true, supportOcticons: true });
119
		data.disposables.add(data.iconLabel);
120

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

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

126
		const configureFont = () => {
J
Joao Moreno 已提交
127 128 129 130
			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;
131
			const fontWeight = configuration.fontInfo.fontWeight;
J
Johannes Rieken 已提交
132 133
			const fontSizePx = `${fontSize}px`;
			const lineHeightPx = `${lineHeight}px`;
J
Joao Moreno 已提交
134 135

			data.root.style.fontSize = fontSizePx;
136
			data.root.style.fontWeight = fontWeight;
J
Joao Moreno 已提交
137 138 139 140
			main.style.fontFamily = fontFamily;
			main.style.lineHeight = lineHeightPx;
			data.icon.style.height = lineHeightPx;
			data.icon.style.width = lineHeightPx;
141 142
			data.readMore.style.height = lineHeightPx;
			data.readMore.style.width = lineHeightPx;
143 144 145 146
		};

		configureFont();

147
		data.disposables.add(Event.chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
J
Joao Moreno 已提交
148
			.filter(e => e.fontInfo || e.contribInfo)
149
			.on(configureFont, null));
150

E
Erich Gamma 已提交
151 152 153
		return data;
	}

154
	renderElement(element: CompletionItem, _index: number, templateData: ISuggestionTemplateData): void {
J
Johannes Rieken 已提交
155
		const data = <ISuggestionTemplateData>templateData;
156
		const suggestion = (<CompletionItem>element).completion;
E
Erich Gamma 已提交
157

158
		data.icon.className = 'icon ' + completionKindToCssClass(suggestion.kind);
159 160
		data.colorspan.style.backgroundColor = '';

161 162 163

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

J
Johannes Rieken 已提交
167 168
		let color: string[] = [];
		if (suggestion.kind === CompletionItemKind.Color && extractColor(element, color)) {
169 170
			// special logic for 'color' completion items
			data.icon.className = 'icon customcolor';
J
Johannes Rieken 已提交
171
			data.colorspan.style.backgroundColor = color[0];
E
Erich Gamma 已提交
172

173 174
		} else if (suggestion.kind === CompletionItemKind.File && this._themeService.getIconTheme().hasFileIcons) {
			// special logic for 'file' completion items
175
			data.icon.className = 'icon hide';
M
Matt Bierner 已提交
176
			labelOptions.extraClasses = ([] as string[]).concat(
177 178 179
				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)
			);
180

181 182
		} else if (suggestion.kind === CompletionItemKind.Folder && this._themeService.getIconTheme().hasFolderIcons) {
			// special logic for 'folder' completion items
183
			data.icon.className = 'icon hide';
M
Matt Bierner 已提交
184
			labelOptions.extraClasses = ([] as string[]).concat(
185 186
				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)
187
			);
188 189 190 191 192 193
		} else {
			// normal icon
			data.icon.className = 'icon hide';
			labelOptions.extraClasses = [
				`suggest-icon ${completionKindToCssClass(suggestion.kind)}`
			];
194 195
		}

B
Benjamin Pasero 已提交
196
		data.iconLabel.setLabel(suggestion.label, undefined, labelOptions);
197
		data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, '');
J
Joao Moreno 已提交
198

199
		if (canExpandCompletionItem(element)) {
200 201
			show(data.readMore);
			data.readMore.onmousedown = e => {
202 203 204
				e.stopPropagation();
				e.preventDefault();
			};
205
			data.readMore.onclick = e => {
206 207 208 209 210
				e.stopPropagation();
				e.preventDefault();
				this.widget.toggleDetails();
			};
		} else {
211 212 213
			hide(data.readMore);
			data.readMore.onmousedown = null;
			data.readMore.onclick = null;
214
		}
J
Joao Moreno 已提交
215
	}
J
Joao Moreno 已提交
216

J
Joao Moreno 已提交
217
	disposeTemplate(templateData: ISuggestionTemplateData): void {
218
		templateData.disposables.dispose();
J
Joao Moreno 已提交
219 220
	}
}
E
Erich Gamma 已提交
221

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

231

232
class SuggestionDetails {
J
Joao Moreno 已提交
233 234

	private el: HTMLElement;
235
	private close: HTMLElement;
236
	private scrollbar: DomScrollableElement;
237
	private body: HTMLElement;
238
	private header: HTMLElement;
J
Joao Moreno 已提交
239 240
	private type: HTMLElement;
	private docs: HTMLElement;
M
Matt Bierner 已提交
241
	private ariaLabel: string | null;
242
	private readonly disposables: DisposableStore;
J
Johannes Rieken 已提交
243
	private renderDisposeable?: IDisposable;
244
	private borderWidth: number = 1;
245 246 247

	constructor(
		container: HTMLElement,
248 249 250
		private readonly widget: SuggestWidget,
		private readonly editor: ICodeEditor,
		private readonly markdownRenderer: MarkdownRenderer,
251
		private readonly triggerKeybindingLabel: string,
252
	) {
253
		this.disposables = new DisposableStore();
J
Joao Moreno 已提交
254 255

		this.el = append(container, $('.details'));
256
		this.disposables.add(toDisposable(() => container.removeChild(this.el)));
257

258
		this.body = $('.body');
259

260
		this.scrollbar = new DomScrollableElement(this.body, {});
261
		append(this.el, this.scrollbar.getDomNode());
262
		this.disposables.add(this.scrollbar);
263

264 265
		this.header = append(this.body, $('.header'));
		this.close = append(this.header, $('span.close'));
266
		this.close.title = nls.localize('readLess', "Read less...{0}", this.triggerKeybindingLabel);
267
		this.type = append(this.header, $('p.type'));
268

269
		this.docs = append(this.body, $('p.docs'));
270
		this.ariaLabel = null;
271 272 273

		this.configureFont();

274
		Event.chain<IConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
J
Joao Moreno 已提交
275
			.filter(e => e.fontInfo)
276
			.on(this.configureFont, this, this.disposables);
277

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

J
Joao Moreno 已提交
281 282 283
	get element() {
		return this.el;
	}
J
Joao Moreno 已提交
284

285 286 287 288 289
	renderLoading(): void {
		this.type.textContent = nls.localize('loading', "Loading...");
		this.docs.textContent = '';
	}

290
	renderItem(item: CompletionItem, explainMode: boolean): void {
291
		this.renderDisposeable = dispose(this.renderDisposeable);
292

293 294 295
		let { documentation, detail } = item.completion;
		// --- documentation

296
		if (explainMode) {
297 298 299
			let md = '';
			md += `score: ${item.score[0]}${item.word ? `, compared '${item.completion.filterText && (item.completion.filterText + ' (filterText)') || item.completion.label}' with '${item.word}'` : ' (no prefix)'}\n`;
			md += `distance: ${item.distance}, see localityBonus-setting\n`;
J
Johannes Rieken 已提交
300
			md += `index: ${item.idx}, based on ${item.completion.sortText && `sortText: "${item.completion.sortText}"` || 'label'}\n`;
301
			documentation = new MarkdownString().appendCodeblock('empty', md);
302
			detail = `Provider: ${item.provider._debugDisplayName}`;
303 304
		}

305
		if (!explainMode && !canExpandCompletionItem(item)) {
J
Joao Moreno 已提交
306 307
			this.type.textContent = '';
			this.docs.textContent = '';
308
			addClass(this.el, 'no-docs');
309
			this.ariaLabel = null;
J
Joao Moreno 已提交
310 311
			return;
		}
312
		removeClass(this.el, 'no-docs');
313
		if (typeof documentation === 'string') {
314
			removeClass(this.docs, 'markdown-docs');
315
			this.docs.textContent = documentation;
316
		} else {
317
			addClass(this.docs, 'markdown-docs');
M
Matt Bierner 已提交
318
			this.docs.innerHTML = '';
319
			const renderedContents = this.markdownRenderer.render(documentation);
320
			this.renderDisposeable = renderedContents;
321
			this.docs.appendChild(renderedContents.element);
322
		}
323

324 325 326
		// --- details
		if (detail) {
			this.type.innerText = detail;
R
Ramya Achutha Rao 已提交
327 328 329 330 331 332
			show(this.type);
		} else {
			this.type.innerText = '';
			hide(this.type);
		}

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

335
		this.close.onmousedown = e => {
336 337 338
			e.preventDefault();
			e.stopPropagation();
		};
339
		this.close.onclick = e => {
340 341 342 343
			e.preventDefault();
			e.stopPropagation();
			this.widget.toggleDetails();
		};
344

J
Joao Moreno 已提交
345
		this.body.scrollTop = 0;
346
		this.scrollbar.scanDomNode();
347

348 349
		this.ariaLabel = strings.format(
			'{0}{1}',
350 351
			detail || '',
			documentation ? (typeof documentation === 'string' ? documentation : documentation.value) : '');
352 353
	}

M
Matt Bierner 已提交
354
	getAriaLabel() {
355
		return this.ariaLabel;
J
Joao Moreno 已提交
356 357
	}

358 359 360 361 362 363 364 365
	scrollDown(much = 8): void {
		this.body.scrollTop += much;
	}

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

366 367 368 369 370 371 372 373
	scrollTop(): void {
		this.body.scrollTop = 0;
	}

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

374 375 376 377 378 379 380 381
	pageDown(): void {
		this.scrollDown(80);
	}

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

382 383 384 385
	setBorderWidth(width: number): void {
		this.borderWidth = width;
	}

386
	private configureFont() {
J
Joao Moreno 已提交
387 388 389
		const configuration = this.editor.getConfiguration();
		const fontFamily = configuration.fontInfo.fontFamily;
		const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize;
390
		const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
391
		const fontWeight = configuration.fontInfo.fontWeight;
J
Johannes Rieken 已提交
392
		const fontSizePx = `${fontSize}px`;
393
		const lineHeightPx = `${lineHeight}px`;
J
Joao Moreno 已提交
394

J
Joao Moreno 已提交
395
		this.el.style.fontSize = fontSizePx;
396
		this.el.style.fontWeight = fontWeight;
J
Joao Moreno 已提交
397
		this.type.style.fontFamily = fontFamily;
398 399
		this.close.style.height = lineHeightPx;
		this.close.style.width = lineHeightPx;
400
	}
401 402

	dispose(): void {
403
		this.disposables.dispose();
404 405
		this.renderDisposeable = dispose(this.renderDisposeable);
	}
J
Joao Moreno 已提交
406 407
}

408
export interface ISelectedSuggestion {
409
	item: CompletionItem;
410 411 412 413
	index: number;
	model: CompletionModel;
}

414
export class SuggestWidget implements IContentWidget, IListVirtualDelegate<CompletionItem>, IDisposable {
J
Joao Moreno 已提交
415

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

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

J
Joao Moreno 已提交
421
	// Editor.IContentWidget.allowEditorOverflow
422
	readonly allowEditorOverflow = true;
J
Johannes Rieken 已提交
423
	readonly suppressMouseDown = true;
J
Joao Moreno 已提交
424

J
Johannes Rieken 已提交
425 426
	private state: State | null = null;
	private isAuto: boolean = false;
427
	private loadingTimeout: IDisposable = Disposable.None;
J
Johannes Rieken 已提交
428
	private currentSuggestionDetails: CancelablePromise<void> | null = null;
M
Matt Bierner 已提交
429
	private focusedItem: CompletionItem | null;
J
Johannes Rieken 已提交
430 431
	private ignoreFocusEvents: boolean = false;
	private completionModel: CompletionModel | null = null;
J
Joao Moreno 已提交
432

E
Erich Gamma 已提交
433
	private element: HTMLElement;
J
Joao Moreno 已提交
434
	private messageElement: HTMLElement;
J
Joao Moreno 已提交
435
	private listElement: HTMLElement;
J
Joao Moreno 已提交
436
	private details: SuggestionDetails;
437
	private list: List<CompletionItem>;
J
Johannes Rieken 已提交
438
	private listHeight?: number;
E
Erich Gamma 已提交
439

440 441
	private readonly suggestWidgetVisible: IContextKey<boolean>;
	private readonly suggestWidgetMultipleSuggestions: IContextKey<boolean>;
J
Joao Moreno 已提交
442

443
	private readonly showTimeout = new TimeoutTimer();
444
	private readonly toDispose = new DisposableStore();
J
Joao Moreno 已提交
445

446 447
	private onDidSelectEmitter = new Emitter<ISelectedSuggestion>();
	private onDidFocusEmitter = new Emitter<ISelectedSuggestion>();
448 449 450
	private onDidHideEmitter = new Emitter<this>();
	private onDidShowEmitter = new Emitter<this>();

451 452
	readonly onDidSelect: Event<ISelectedSuggestion> = this.onDidSelectEmitter.event;
	readonly onDidFocus: Event<ISelectedSuggestion> = this.onDidFocusEmitter.event;
453 454
	readonly onDidHide: Event<this> = this.onDidHideEmitter.event;
	readonly onDidShow: Event<this> = this.onDidShowEmitter.event;
455

456
	private readonly maxWidgetWidth = 660;
457
	private readonly listWidth = 330;
458
	private readonly storageService: IStorageService;
J
Johannes Rieken 已提交
459 460
	private detailsFocusBorderColor?: string;
	private detailsBorderColor?: string;
461

462 463
	private firstFocusInCurrentList: boolean = false;

464
	private preferDocPositionTop: boolean = false;
J
Johannes Rieken 已提交
465
	private docsPositionPreviousWidgetY: number | null = null;
466
	private explainMode: boolean = false;
467

468
	constructor(
469
		private readonly editor: ICodeEditor,
470
		@ITelemetryService private readonly telemetryService: ITelemetryService,
471
		@IContextKeyService contextKeyService: IContextKeyService,
472
		@IThemeService themeService: IThemeService,
473
		@IStorageService storageService: IStorageService,
474 475
		@IKeybindingService keybindingService: IKeybindingService,
		@IModeService modeService: IModeService,
476 477
		@IOpenerService openerService: IOpenerService,
		@IInstantiationService instantiationService: IInstantiationService,
478
	) {
479 480
		const kb = keybindingService.lookupKeybinding('editor.action.triggerSuggest');
		const triggerKeybindingLabel = !kb ? '' : ` (${kb.getLabel()})`;
481
		const markdownRenderer = this.toDispose.add(new MarkdownRenderer(editor, modeService, openerService));
482

E
Erich Gamma 已提交
483
		this.isAuto = false;
J
Joao Moreno 已提交
484
		this.focusedItem = null;
485
		this.storageService = storageService;
E
Erich Gamma 已提交
486

487
		this.element = $('.editor-widget.suggest-widget');
488
		this.toDispose.add(addDisposableListener(this.element, 'click', e => {
J
Johannes Rieken 已提交
489 490 491 492
			if (e.target === this.element) {
				this.hideWidget();
			}
		}));
493

J
Joao Moreno 已提交
494
		this.messageElement = append(this.element, $('.message'));
J
Joao Moreno 已提交
495
		this.listElement = append(this.element, $('.tree'));
496
		this.details = instantiationService.createInstance(SuggestionDetails, this.element, this, this.editor, markdownRenderer, triggerKeybindingLabel);
J
Joao Moreno 已提交
497

J
Johannes Rieken 已提交
498 499 500
		const applyIconStyle = () => toggleClass(this.element, 'no-icons', !this.editor.getConfiguration().contribInfo.suggest.showIcons);
		applyIconStyle();

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

503 504
		this.list = new List(this.listElement, this, [renderer], {
			useShadows: false,
J
Joao Moreno 已提交
505 506
			openController: { shouldOpen: () => false },
			mouseSupport: false
507
		});
J
Joao Moreno 已提交
508

509 510 511 512 513 514 515 516 517 518 519 520
		this.toDispose.add(attachListStyler(this.list, themeService, {
			listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,
			listInactiveFocusOutline: activeContrastBorder
		}));
		this.toDispose.add(themeService.onThemeChange(t => this.onThemeChange(t)));
		this.toDispose.add(editor.onDidLayoutChange(() => this.onEditorLayoutChange()));
		this.toDispose.add(this.list.onMouseDown(e => this.onListMouseDown(e)));
		this.toDispose.add(this.list.onSelectionChange(e => this.onListSelection(e)));
		this.toDispose.add(this.list.onFocusChange(e => this.onListFocus(e)));
		this.toDispose.add(this.editor.onDidChangeCursorSelection(() => this.onCursorSelectionChanged()));
		this.toDispose.add(this.editor.onDidChangeConfiguration(e => e.contribInfo && applyIconStyle()));

J
Joao Moreno 已提交
521

522 523
		this.suggestWidgetVisible = SuggestContext.Visible.bindTo(contextKeyService);
		this.suggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(contextKeyService);
J
Joao Moreno 已提交
524

J
Joao Moreno 已提交
525 526
		this.editor.addContentWidget(this);
		this.setState(State.Hidden);
527

M
Martin Aeschlimann 已提交
528
		this.onThemeChange(themeService.getTheme());
J
Joao Moreno 已提交
529
	}
E
Erich Gamma 已提交
530

J
Joao Moreno 已提交
531 532 533 534
	private onCursorSelectionChanged(): void {
		if (this.state === State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
535

J
Joao Moreno 已提交
536 537
		this.editor.layoutContentWidget(this);
	}
J
Joao Moreno 已提交
538

539 540 541 542 543 544
	private onEditorLayoutChange(): void {
		if ((this.state === State.Open || this.state === State.Details) && this.expandDocsSettingFromStorage()) {
			this.expandSideOrBelow();
		}
	}

J
Joao Moreno 已提交
545
	private onListMouseDown(e: IListMouseEvent<CompletionItem>): void {
J
Joao Moreno 已提交
546 547 548 549
		if (typeof e.element === 'undefined' || typeof e.index === 'undefined') {
			return;
		}

J
Joao Moreno 已提交
550 551 552 553
		// prevent stealing browser focus from the editor
		e.browserEvent.preventDefault();
		e.browserEvent.stopPropagation();

J
Joao Moreno 已提交
554 555 556
		this.select(e.element, e.index);
	}

557
	private onListSelection(e: IListEvent<CompletionItem>): void {
J
Joao Moreno 已提交
558 559 560
		if (!e.elements.length) {
			return;
		}
E
Erich Gamma 已提交
561

J
Joao Moreno 已提交
562 563 564 565
		this.select(e.elements[0], e.indexes[0]);
	}

	private select(item: CompletionItem, index: number): void {
M
Matt Bierner 已提交
566
		const completionModel = this.completionModel;
J
Joao Moreno 已提交
567

M
Matt Bierner 已提交
568 569 570 571
		if (!completionModel) {
			return;
		}

572 573
		this.onDidSelectEmitter.fire({ item, index, model: completionModel });
		this.editor.focus();
J
Joao Moreno 已提交
574
	}
E
Erich Gamma 已提交
575

576
	private _getSuggestionAriaAlertLabel(item: CompletionItem): string {
577 578
		if (this.expandDocsSettingFromStorage()) {
			return nls.localize('ariaCurrenttSuggestionReadDetails', "Item {0}, docs: {1}", item.completion.label, this.details.getAriaLabel());
579
		} else {
580
			return item.completion.label;
581 582 583
		}
	}

J
Johannes Rieken 已提交
584
	private _lastAriaAlertLabel: string | null = null;
M
Matt Bierner 已提交
585
	private _ariaAlert(newAriaAlertLabel: string | null): void {
586 587 588 589 590
		if (this._lastAriaAlertLabel === newAriaAlertLabel) {
			return;
		}
		this._lastAriaAlertLabel = newAriaAlertLabel;
		if (this._lastAriaAlertLabel) {
591
			alert(this._lastAriaAlertLabel, true);
592 593 594
		}
	}

M
Martin Aeschlimann 已提交
595
	private onThemeChange(theme: ITheme) {
M
Matt Bierner 已提交
596
		const backgroundColor = theme.getColor(editorSuggestWidgetBackground);
M
Martin Aeschlimann 已提交
597
		if (backgroundColor) {
598 599
			this.listElement.style.backgroundColor = backgroundColor.toString();
			this.details.element.style.backgroundColor = backgroundColor.toString();
R
Ramya Achutha Rao 已提交
600
			this.messageElement.style.backgroundColor = backgroundColor.toString();
M
Martin Aeschlimann 已提交
601
		}
M
Matt Bierner 已提交
602
		const borderColor = theme.getColor(editorSuggestWidgetBorder);
M
Martin Aeschlimann 已提交
603
		if (borderColor) {
604 605 606
			this.listElement.style.borderColor = borderColor.toString();
			this.details.element.style.borderColor = borderColor.toString();
			this.messageElement.style.borderColor = borderColor.toString();
607
			this.detailsBorderColor = borderColor.toString();
M
Martin Aeschlimann 已提交
608
		}
M
Matt Bierner 已提交
609
		const focusBorderColor = theme.getColor(focusBorder);
610 611 612
		if (focusBorderColor) {
			this.detailsFocusBorderColor = focusBorderColor.toString();
		}
613
		this.details.setBorderWidth(theme.type === 'hc' ? 2 : 1);
M
Martin Aeschlimann 已提交
614 615
	}

616
	private onListFocus(e: IListEvent<CompletionItem>): void {
617 618 619 620
		if (this.ignoreFocusEvents) {
			return;
		}

J
Joao Moreno 已提交
621
		if (!e.elements.length) {
J
Joao Moreno 已提交
622 623 624 625 626
			if (this.currentSuggestionDetails) {
				this.currentSuggestionDetails.cancel();
				this.currentSuggestionDetails = null;
				this.focusedItem = null;
			}
627

J
Joao Moreno 已提交
628
			this._ariaAlert(null);
J
Joao Moreno 已提交
629 630
			return;
		}
E
Erich Gamma 已提交
631

M
Matt Bierner 已提交
632 633 634 635
		if (!this.completionModel) {
			return;
		}

J
Joao Moreno 已提交
636
		const item = e.elements[0];
J
Johannes Rieken 已提交
637
		const index = e.indexes[0];
638

639
		this.firstFocusInCurrentList = !this.focusedItem;
J
Johannes Rieken 已提交
640
		if (item !== this.focusedItem) {
E
Erich Gamma 已提交
641

J
Joao Moreno 已提交
642

J
Johannes Rieken 已提交
643 644 645 646
			if (this.currentSuggestionDetails) {
				this.currentSuggestionDetails.cancel();
				this.currentSuggestionDetails = null;
			}
E
Erich Gamma 已提交
647

J
Johannes Rieken 已提交
648
			this.focusedItem = item;
J
Joao Moreno 已提交
649

J
Johannes Rieken 已提交
650
			this.list.reveal(index);
651

652 653 654 655 656 657 658
			this.currentSuggestionDetails = createCancelablePromise(async token => {
				const loading = disposableTimeout(() => this.showDetails(true), 250);
				token.onCancellationRequested(() => loading.dispose());
				const result = await item.resolve(token);
				loading.dispose();
				return result;
			});
659

J
Johannes Rieken 已提交
660
			this.currentSuggestionDetails.then(() => {
661
				if (index >= this.list.length || item !== this.list.element(index)) {
J
Johannes Rieken 已提交
662 663
					return;
				}
664

J
Johannes Rieken 已提交
665 666 667 668 669
				// item can have extra information, so re-render
				this.ignoreFocusEvents = true;
				this.list.splice(index, 1, [item]);
				this.list.setFocus([index]);
				this.ignoreFocusEvents = false;
670

J
Johannes Rieken 已提交
671
				if (this.expandDocsSettingFromStorage()) {
672
					this.showDetails(false);
J
Johannes Rieken 已提交
673 674 675 676 677
				} else {
					removeClass(this.element, 'docs-side');
				}

				this._ariaAlert(this._getSuggestionAriaAlertLabel(item));
678
			}).catch(onUnexpectedError);
J
Johannes Rieken 已提交
679
		}
680 681

		// emit an event
682
		this.onDidFocusEmitter.fire({ item, index, model: this.completionModel });
J
Joao Moreno 已提交
683
	}
J
Joao Moreno 已提交
684 685

	private setState(state: State): void {
J
Joao Moreno 已提交
686 687 688 689
		if (!this.element) {
			return;
		}

690
		const stateChanged = this.state !== state;
J
Joao Moreno 已提交
691 692
		this.state = state;

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

J
Joao Moreno 已提交
695 696
		switch (state) {
			case State.Hidden:
697
				hide(this.messageElement, this.details.element, this.listElement);
J
Joao Moreno 已提交
698
				this.hide();
699
				this.listHeight = 0;
A
tslint  
Alex Dima 已提交
700
				if (stateChanged) {
701
					this.list.splice(0, this.list.length);
A
tslint  
Alex Dima 已提交
702
				}
703
				this.focusedItem = null;
704
				break;
J
Joao Moreno 已提交
705
			case State.Loading:
J
Joao Moreno 已提交
706
				this.messageElement.textContent = SuggestWidget.LOADING_MESSAGE;
J
Joao Moreno 已提交
707
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
708
				show(this.messageElement);
R
Ramya Achutha Rao 已提交
709
				removeClass(this.element, 'docs-side');
710
				this.show();
711
				this.focusedItem = null;
J
Joao Moreno 已提交
712 713
				break;
			case State.Empty:
J
Joao Moreno 已提交
714
				this.messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE;
J
Joao Moreno 已提交
715
				hide(this.listElement, this.details.element);
J
Joao Moreno 已提交
716
				show(this.messageElement);
R
Ramya Achutha Rao 已提交
717
				removeClass(this.element, 'docs-side');
718
				this.show();
719
				this.focusedItem = null;
J
Joao Moreno 已提交
720 721
				break;
			case State.Open:
722
				hide(this.messageElement);
723
				show(this.listElement);
724
				this.show();
J
Joao Moreno 已提交
725 726
				break;
			case State.Frozen:
727
				hide(this.messageElement);
J
Joao Moreno 已提交
728
				show(this.listElement);
729
				this.show();
J
Joao Moreno 已提交
730 731
				break;
			case State.Details:
732 733
				hide(this.messageElement);
				show(this.details.element, this.listElement);
734
				this.show();
735
				this._ariaAlert(this.details.getAriaLabel());
J
Joao Moreno 已提交
736 737
				break;
		}
738 739
	}

740
	showTriggered(auto: boolean, delay: number) {
J
Joao Moreno 已提交
741 742 743
		if (this.state !== State.Hidden) {
			return;
		}
E
Erich Gamma 已提交
744

745
		this.isAuto = !!auto;
J
Joao Moreno 已提交
746 747

		if (!this.isAuto) {
748
			this.loadingTimeout = disposableTimeout(() => this.setState(State.Loading), delay);
J
Joao Moreno 已提交
749
		}
750
	}
E
Erich Gamma 已提交
751

752
	showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean): void {
753 754 755
		this.preferDocPositionTop = false;
		this.docsPositionPreviousWidgetY = null;

756
		this.loadingTimeout.dispose();
J
Joao Moreno 已提交
757

J
Johannes Rieken 已提交
758 759 760 761 762
		if (this.currentSuggestionDetails) {
			this.currentSuggestionDetails.cancel();
			this.currentSuggestionDetails = null;
		}

763 764 765
		if (this.completionModel !== completionModel) {
			this.completionModel = completionModel;
		}
J
Joao Moreno 已提交
766

R
Ramya Achutha Rao 已提交
767
		if (isFrozen && this.state !== State.Empty && this.state !== State.Hidden) {
768 769
			this.setState(State.Frozen);
			return;
770 771
		}

772 773
		let visibleCount = this.completionModel.items.length;

J
Joao Moreno 已提交
774
		const isEmpty = visibleCount === 0;
775
		this.suggestWidgetMultipleSuggestions.set(visibleCount > 1);
J
Joao Moreno 已提交
776 777

		if (isEmpty) {
778
			if (isAuto) {
J
Joao Moreno 已提交
779 780
				this.setState(State.Hidden);
			} else {
Y
Yuki Ueda 已提交
781
				this.setState(State.Empty);
J
Joao Moreno 已提交
782 783
			}

J
Joao Moreno 已提交
784
			this.completionModel = null;
J
Joao Moreno 已提交
785

J
Joao Moreno 已提交
786
		} else {
787 788 789 790 791 792 793 794

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

J
Johannes Rieken 已提交
802
			this.focusedItem = null;
803 804
			this.list.splice(0, this.list.length, this.completionModel.items);

R
Ramya Achutha Rao 已提交
805 806 807 808 809
			if (isFrozen) {
				this.setState(State.Frozen);
			} else {
				this.setState(State.Open);
			}
R
Ramya Achutha Rao 已提交
810

J
Johannes Rieken 已提交
811
			this.list.reveal(selectionIndex, 0);
J
Joao Moreno 已提交
812 813
			this.list.setFocus([selectionIndex]);

R
Ramya Achutha Rao 已提交
814 815 816 817
			// Reset focus border
			if (this.detailsBorderColor) {
				this.details.element.style.borderColor = this.detailsBorderColor;
			}
J
Joao Moreno 已提交
818
		}
819
	}
E
Erich Gamma 已提交
820

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

J
Joao Moreno 已提交
836
	selectNext(): boolean {
J
Joao Moreno 已提交
837 838 839 840 841 842 843 844 845
		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 已提交
846 847
	}

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

J
Joao Moreno 已提交
863
	selectPreviousPage(): boolean {
J
Joao Moreno 已提交
864 865 866 867 868 869 870 871 872 873 874 875
		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 已提交
876 877
	}

J
Joao Moreno 已提交
878
	selectPrevious(): boolean {
J
Joao Moreno 已提交
879 880 881 882 883 884 885
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusPrevious(1, true);
J
Joao Moreno 已提交
886
				return false;
J
Joao Moreno 已提交
887
		}
E
Erich Gamma 已提交
888 889
	}

890 891 892 893 894
	selectFirst(): boolean {
		switch (this.state) {
			case State.Hidden:
				return false;
			case State.Details:
895
				this.details.scrollTop();
896 897 898 899 900 901 902 903 904
				return true;
			case State.Loading:
				return !this.isAuto;
			default:
				this.list.focusFirst();
				return true;
		}
	}

M
Matt Bierner 已提交
905
	getFocusedItem(): ISelectedSuggestion | undefined {
906 907
		if (this.state !== State.Hidden
			&& this.state !== State.Empty
M
Matt Bierner 已提交
908 909 910
			&& this.state !== State.Loading
			&& this.completionModel
		) {
911

912 913 914 915 916
			return {
				item: this.list.getFocusedElements()[0],
				index: this.list.getFocus()[0],
				model: this.completionModel
			};
J
Joao Moreno 已提交
917
		}
918
		return undefined;
E
Erich Gamma 已提交
919 920
	}

921
	toggleDetailsFocus(): void {
J
Joao Moreno 已提交
922 923
		if (this.state === State.Details) {
			this.setState(State.Open);
924 925 926
			if (this.detailsBorderColor) {
				this.details.element.style.borderColor = this.detailsBorderColor;
			}
927
		} else if (this.state === State.Open && this.expandDocsSettingFromStorage()) {
928
			this.setState(State.Details);
929 930 931
			if (this.detailsFocusBorderColor) {
				this.details.element.style.borderColor = this.detailsFocusBorderColor;
			}
J
Joao Moreno 已提交
932
		}
933
		this.telemetryService.publicLog2('suggestWidget:toggleDetailsFocus');
934
	}
J
Joao Moreno 已提交
935

936
	toggleDetails(): void {
R
Ramya Achutha Rao 已提交
937 938 939
		if (!canExpandCompletionItem(this.list.getFocusedElements()[0])) {
			return;
		}
940

941 942
		if (this.expandDocsSettingFromStorage()) {
			this.updateExpandDocsSetting(false);
943
			hide(this.details.element);
944
			removeClass(this.element, 'docs-side');
945
			removeClass(this.element, 'docs-below');
946
			this.editor.layoutContentWidget(this);
947
			this.telemetryService.publicLog2('suggestWidget:collapseDetails');
948
		} else {
949
			if (this.state !== State.Open && this.state !== State.Details && this.state !== State.Frozen) {
950 951 952
				return;
			}

953
			this.updateExpandDocsSetting(true);
J
Johannes Rieken 已提交
954
			this.showDetails(false);
955
			this._ariaAlert(this.details.getAriaLabel());
956
			this.telemetryService.publicLog2('suggestWidget:expandDetails');
957 958 959
		}
	}

960
	showDetails(loading: boolean): void {
R
Ramya Achutha Rao 已提交
961
		this.expandSideOrBelow();
962 963

		show(this.details.element);
964

965
		this.details.element.style.maxHeight = this.maxWidgetHeight + 'px';
966

967 968 969
		if (loading) {
			this.details.renderLoading();
		} else {
970
			this.details.renderItem(this.list.getFocusedElements()[0], this.explainMode);
971
		}
972

R
Ramya Achutha Rao 已提交
973 974
		// Reset margin-top that was set as Fix for #26416
		this.listElement.style.marginTop = '0px';
975 976 977 978 979

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

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

J
Joao Moreno 已提交
981
		this.editor.focus();
J
Joao Moreno 已提交
982 983
	}

984 985 986 987 988 989 990
	toggleExplainMode(): void {
		if (this.list.getFocusedElements()[0] && this.expandDocsSettingFromStorage()) {
			this.explainMode = !this.explainMode;
			this.showDetails(false);
		}
	}

991
	private show(): void {
992 993 994 995 996 997
		const newHeight = this.updateListHeight();
		if (newHeight !== this.listHeight) {
			this.editor.layoutContentWidget(this);
			this.listHeight = newHeight;
		}

J
Joao Moreno 已提交
998
		this.suggestWidgetVisible.set(true);
999

1000
		this.showTimeout.cancelAndSet(() => {
J
Joao Moreno 已提交
1001
			addClass(this.element, 'visible');
1002
			this.onDidShowEmitter.fire(this);
1003
		}, 100);
E
Erich Gamma 已提交
1004 1005
	}

1006
	private hide(): void {
J
Joao Moreno 已提交
1007
		this.suggestWidgetVisible.reset();
S
Sean Kelly 已提交
1008
		this.suggestWidgetMultipleSuggestions.reset();
J
Joao Moreno 已提交
1009
		removeClass(this.element, 'visible');
E
Erich Gamma 已提交
1010 1011
	}

1012
	hideWidget(): void {
1013
		this.loadingTimeout.dispose();
1014
		this.setState(State.Hidden);
1015
		this.onDidHideEmitter.fire(this);
1016 1017
	}

M
Matt Bierner 已提交
1018
	getPosition(): IContentWidgetPosition | null {
J
Joao Moreno 已提交
1019 1020
		if (this.state === State.Hidden) {
			return null;
E
Erich Gamma 已提交
1021
		}
J
Joao Moreno 已提交
1022

1023 1024 1025 1026 1027
		let preference = [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE];
		if (this.preferDocPositionTop) {
			preference = [ContentWidgetPositionPreference.ABOVE];
		}

J
Joao Moreno 已提交
1028
		return {
1029
			position: this.editor.getPosition(),
1030
			preference: preference
J
Joao Moreno 已提交
1031
		};
E
Erich Gamma 已提交
1032 1033
	}

J
Joao Moreno 已提交
1034
	getDomNode(): HTMLElement {
E
Erich Gamma 已提交
1035 1036 1037
		return this.element;
	}

J
Joao Moreno 已提交
1038
	getId(): string {
E
Erich Gamma 已提交
1039 1040 1041
		return SuggestWidget.ID;
	}

1042
	private updateListHeight(): number {
J
Joao Moreno 已提交
1043
		let height = 0;
E
Erich Gamma 已提交
1044

J
Joao Moreno 已提交
1045
		if (this.state === State.Empty || this.state === State.Loading) {
1046
			height = this.unfocusedHeight;
J
Joao Moreno 已提交
1047
		} else {
1048
			const suggestionCount = this.list.contentHeight / this.unfocusedHeight;
J
Johannes Rieken 已提交
1049 1050
			const { maxVisibleSuggestions } = this.editor.getConfiguration().contribInfo.suggest;
			height = Math.min(suggestionCount, maxVisibleSuggestions) * this.unfocusedHeight;
J
Joao Moreno 已提交
1051
		}
J
Joao Moreno 已提交
1052

1053
		this.element.style.lineHeight = `${this.unfocusedHeight}px`;
1054
		this.listElement.style.height = `${height}px`;
J
Joao Moreno 已提交
1055
		this.list.layout(height);
1056
		return height;
J
Joao Moreno 已提交
1057 1058
	}

R
Ramya Achutha Rao 已提交
1059 1060 1061
	/**
	 * Adds the propert classes, margins when positioning the docs to the side
	 */
1062
	private adjustDocsPosition() {
M
Matt Bierner 已提交
1063 1064 1065 1066
		if (!this.editor.hasModel()) {
			return;
		}

R
Ramya Achutha Rao 已提交
1067
		const lineHeight = this.editor.getConfiguration().fontInfo.lineHeight;
1068 1069 1070 1071
		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;
1072 1073 1074
		const widgetCoords = getDomNodePagePosition(this.element);
		const widgetX = widgetCoords.left;
		const widgetY = widgetCoords.top;
1075

1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
		// Fixes #27649
		// Check if the Y changed to the top of the cursor and keep the widget flagged to prefer top
		if (this.docsPositionPreviousWidgetY &&
			this.docsPositionPreviousWidgetY < widgetY &&
			!this.preferDocPositionTop) {
			this.preferDocPositionTop = true;
			this.adjustDocsPosition();
			return;
		}
		this.docsPositionPreviousWidgetY = widgetY;

1087 1088 1089
		if (widgetX < cursorX - this.listWidth) {
			// Widget is too far to the left of cursor, swap list and docs
			addClass(this.element, 'list-right');
1090 1091
		} else {
			removeClass(this.element, 'list-right');
1092 1093
		}

R
Ramya Achutha Rao 已提交
1094 1095 1096
		// 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 已提交
1097
		if (hasClass(this.element, 'docs-side')
R
Ramya Achutha Rao 已提交
1098
			&& cursorY - lineHeight > widgetY
R
Ramya Achutha Rao 已提交
1099 1100 1101 1102 1103
			&& 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`;
1104
		}
1105 1106
	}

R
Ramya Achutha Rao 已提交
1107 1108 1109
	/**
	 * Adds the proper classes for positioning the docs to the side or below
	 */
1110
	private expandSideOrBelow() {
1111 1112 1113 1114 1115 1116
		if (!canExpandCompletionItem(this.focusedItem) && this.firstFocusInCurrentList) {
			removeClass(this.element, 'docs-side');
			removeClass(this.element, 'docs-below');
			return;
		}

M
Matt Bierner 已提交
1117
		let matches = this.element.style.maxWidth!.match(/(\d+)px/);
1118 1119 1120
		if (!matches || Number(matches[1]) < this.maxWidgetWidth) {
			addClass(this.element, 'docs-below');
			removeClass(this.element, 'docs-side');
1121
		} else if (canExpandCompletionItem(this.focusedItem)) {
1122 1123 1124 1125 1126
			addClass(this.element, 'docs-side');
			removeClass(this.element, 'docs-below');
		}
	}

1127 1128
	// Heights

1129
	private get maxWidgetHeight(): number {
J
Johannes Rieken 已提交
1130
		return this.unfocusedHeight * this.editor.getConfiguration().contribInfo.suggest.maxVisibleSuggestions;
1131 1132 1133
	}

	private get unfocusedHeight(): number {
J
Joao Moreno 已提交
1134 1135
		const configuration = this.editor.getConfiguration();
		return configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight;
1136 1137 1138 1139
	}

	// IDelegate

1140
	getHeight(element: CompletionItem): number {
1141 1142 1143
		return this.unfocusedHeight;
	}

1144
	getTemplateId(element: CompletionItem): string {
1145 1146 1147
		return 'suggestion';
	}

1148
	private expandDocsSettingFromStorage(): boolean {
1149
		return this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, expandSuggestionDocsByDefault);
1150 1151 1152
	}

	private updateExpandDocsSetting(value: boolean) {
1153
		this.storageService.store('expandSuggestionDocs', value, StorageScope.GLOBAL);
1154 1155
	}

J
Joao Moreno 已提交
1156
	dispose(): void {
J
Joao Moreno 已提交
1157
		this.details.dispose();
J
Joao Moreno 已提交
1158
		this.list.dispose();
1159 1160
		this.toDispose.dispose();
		this.loadingTimeout.dispose();
1161
		this.showTimeout.dispose();
E
Erich Gamma 已提交
1162
	}
J
Johannes Rieken 已提交
1163
}
1164 1165

registerThemingParticipant((theme, collector) => {
M
Matt Bierner 已提交
1166
	const matchHighlight = theme.getColor(editorSuggestWidgetHighlightForeground);
1167
	if (matchHighlight) {
J
Johannes Rieken 已提交
1168
		collector.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${matchHighlight}; }`);
1169
	}
M
Matt Bierner 已提交
1170
	const foreground = theme.getColor(editorSuggestWidgetForeground);
1171
	if (foreground) {
1172
		collector.addRule(`.monaco-editor .suggest-widget { color: ${foreground}; }`);
1173
	}
M
Matt Bierner 已提交
1174 1175 1176 1177 1178

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

M
Matt Bierner 已提交
1180
	const codeBackground = theme.getColor(textCodeBlockBackground);
1181 1182 1183
	if (codeBackground) {
		collector.addRule(`.monaco-editor .suggest-widget code { background-color: ${codeBackground}; }`);
	}
1184
});