parameterHintsWidget.ts 12.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.
 *--------------------------------------------------------------------------------------------*/

J
Joao Moreno 已提交
6
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
7
import { domEvent, stop } from 'vs/base/browser/event';
8
import * as aria from 'vs/base/browser/ui/aria/aria';
J
Johannes Rieken 已提交
9
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
10
import { Event } from 'vs/base/common/event';
M
Matt Bierner 已提交
11
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
12 13
import 'vs/css!./parameterHints';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
14
import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions';
15
import * as modes from 'vs/editor/common/modes';
16
import { IModeService } from 'vs/editor/common/services/modeService';
17
import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
18 19 20 21
import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp';
import * as nls from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IOpenerService } from 'vs/platform/opener/common/opener';
22
import { editorHoverBackground, editorHoverBorder, textCodeBlockBackground, textLinkForeground, editorHoverForeground } from 'vs/platform/theme/common/colorRegistry';
23 24
import { HIGH_CONTRAST, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { ParameterHintsModel, TriggerContext } from 'vs/editor/contrib/parameterHints/parameterHintsModel';
25
import { pad } from 'vs/base/common/strings';
E
Erich Gamma 已提交
26

J
Joao Moreno 已提交
27
const $ = dom.$;
J
Joao Moreno 已提交
28

M
Matt Bierner 已提交
29
export class ParameterHintsWidget extends Disposable implements IContentWidget {
E
Erich Gamma 已提交
30

31
	private static readonly ID = 'editor.widget.parameterHintsWidget';
E
Erich Gamma 已提交
32

M
Matt Bierner 已提交
33
	private readonly markdownRenderer: MarkdownRenderer;
34
	private readonly renderDisposeables = this._register(new DisposableStore());
M
Matt Bierner 已提交
35
	private readonly model: ParameterHintsModel;
M
Matt Bierner 已提交
36 37
	private readonly keyVisible: IContextKey<boolean>;
	private readonly keyMultipleSignatures: IContextKey<boolean>;
38 39 40 41 42 43 44 45 46 47 48

	private domNodes?: {
		readonly element: HTMLElement;
		readonly signature: HTMLElement;
		readonly docs: HTMLElement;
		readonly overloads: HTMLElement;
		readonly scrollbar: DomScrollableElement;
	};

	private visible: boolean = false;
	private announcedLabel: string | null = null;
E
Erich Gamma 已提交
49 50

	// Editor.IContentWidget.allowEditorOverflow
J
Joao Moreno 已提交
51
	allowEditorOverflow = true;
E
Erich Gamma 已提交
52

53
	constructor(
54
		private readonly editor: ICodeEditor,
55 56 57 58
		@IContextKeyService contextKeyService: IContextKeyService,
		@IOpenerService openerService: IOpenerService,
		@IModeService modeService: IModeService,
	) {
59
		super();
60
		this.markdownRenderer = this._register(new MarkdownRenderer(editor, modeService, openerService));
M
Matt Bierner 已提交
61
		this.model = this._register(new ParameterHintsModel(editor));
62 63
		this.keyVisible = Context.Visible.bindTo(contextKeyService);
		this.keyMultipleSignatures = Context.MultipleSignatures.bindTo(contextKeyService);
64

M
Matt Bierner 已提交
65
		this._register(this.model.onChangedHints(newParameterHints => {
66 67 68 69 70 71
			if (newParameterHints) {
				this.show();
				this.render(newParameterHints);
			} else {
				this.hide();
			}
J
Joao Moreno 已提交
72
		}));
R
rebornix 已提交
73
	}
E
Erich Gamma 已提交
74

R
rebornix 已提交
75
	private createParamaterHintDOMNodes() {
76 77
		const element = $('.editor-widget.parameter-hints-widget');
		const wrapper = dom.append(element, $('.wrapper'));
78
		wrapper.tabIndex = -1;
J
Joao Moreno 已提交
79

80
		const controls = dom.append(wrapper, $('.controls'));
81
		const previous = dom.append(controls, $('.button.codicon.codicon-chevron-up'));
82
		const overloads = dom.append(controls, $('.overloads'));
83
		const next = dom.append(controls, $('.button.codicon.codicon-chevron-down'));
E
Erich Gamma 已提交
84

J
Joao Moreno 已提交
85
		const onPreviousClick = stop(domEvent(previous, 'click'));
86
		this._register(onPreviousClick(this.previous, this));
E
Erich Gamma 已提交
87

J
Joao Moreno 已提交
88
		const onNextClick = stop(domEvent(next, 'click'));
89
		this._register(onNextClick(this.next, this));
E
Erich Gamma 已提交
90

J
Joao Moreno 已提交
91
		const body = $('.body');
92 93 94
		const scrollbar = new DomScrollableElement(body, {});
		this._register(scrollbar);
		wrapper.appendChild(scrollbar.getDomNode());
J
Joao Moreno 已提交
95

96 97
		const signature = dom.append(body, $('.signature'));
		const docs = dom.append(body, $('.docs'));
J
Joao Moreno 已提交
98

99 100 101 102 103 104 105 106 107
		element.style.userSelect = 'text';

		this.domNodes = {
			element,
			signature,
			overloads,
			docs,
			scrollbar,
		};
J
Joao Moreno 已提交
108

E
Erich Gamma 已提交
109 110 111
		this.editor.addContentWidget(this);
		this.hide();

112
		this._register(this.editor.onDidChangeCursorSelection(e => {
J
Joao Moreno 已提交
113
			if (this.visible) {
114 115
				this.editor.layoutContentWidget(this);
			}
E
Erich Gamma 已提交
116
		}));
J
Joao Moreno 已提交
117 118

		const updateFont = () => {
119 120 121
			if (!this.domNodes) {
				return;
			}
122
			const fontInfo = this.editor.getOption(EditorOption.fontInfo);
123
			this.domNodes.element.style.fontSize = `${fontInfo.fontSize}px`;
J
Joao Moreno 已提交
124 125 126 127
		};

		updateFont();

128 129
		this._register(Event.chain<ConfigurationChangedEvent>(this.editor.onDidChangeConfiguration.bind(this.editor))
			.filter(e => e.hasChanged(EditorOption.fontInfo))
130
			.on(updateFont, null));
J
Joao Moreno 已提交
131

132
		this._register(this.editor.onDidLayoutChange(e => this.updateMaxHeight()));
J
Joao Moreno 已提交
133
		this.updateMaxHeight();
E
Erich Gamma 已提交
134 135 136
	}

	private show(): void {
M
Matt Bierner 已提交
137
		if (this.visible) {
138 139
			return;
		}
E
Erich Gamma 已提交
140

141
		if (!this.domNodes) {
R
rebornix 已提交
142 143 144
			this.createParamaterHintDOMNodes();
		}

145
		this.keyVisible.set(true);
J
Joao Moreno 已提交
146
		this.visible = true;
147 148 149 150 151
		setTimeout(() => {
			if (this.domNodes) {
				dom.addClass(this.domNodes.element, 'visible');
			}
		}, 100);
E
Erich Gamma 已提交
152 153 154 155
		this.editor.layoutContentWidget(this);
	}

	private hide(): void {
M
Matt Bierner 已提交
156
		if (!this.visible) {
157 158
			return;
		}
R
rebornix 已提交
159

160
		this.keyVisible.reset();
J
Joao Moreno 已提交
161
		this.visible = false;
162
		this.announcedLabel = null;
163 164 165
		if (this.domNodes) {
			dom.removeClass(this.domNodes.element, 'visible');
		}
E
Erich Gamma 已提交
166 167 168
		this.editor.layoutContentWidget(this);
	}

P
Peng Lyu 已提交
169
	getPosition(): IContentWidgetPosition | null {
J
Joao Moreno 已提交
170
		if (this.visible) {
E
Erich Gamma 已提交
171 172
			return {
				position: this.editor.getPosition(),
A
Alex Dima 已提交
173
				preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]
E
Erich Gamma 已提交
174 175 176 177 178
			};
		}
		return null;
	}

179
	private render(hints: modes.SignatureHelp): void {
180 181 182 183
		if (!this.domNodes) {
			return;
		}

184
		const multiple = hints.signatures.length > 1;
185
		dom.toggleClass(this.domNodes.element, 'multiple', multiple);
J
Joao Moreno 已提交
186
		this.keyMultipleSignatures.set(multiple);
E
Erich Gamma 已提交
187

188 189
		this.domNodes.signature.innerHTML = '';
		this.domNodes.docs.innerHTML = '';
E
Erich Gamma 已提交
190

191
		const signature = hints.signatures[hints.activeSignature];
J
Joao Moreno 已提交
192 193 194 195
		if (!signature) {
			return;
		}

196
		const code = dom.append(this.domNodes.signature, $('.code'));
197
		const fontInfo = this.editor.getOption(EditorOption.fontInfo);
J
Joao Moreno 已提交
198 199
		code.style.fontSize = `${fontInfo.fontSize}px`;
		code.style.fontFamily = fontInfo.fontFamily;
E
Erich Gamma 已提交
200

201 202 203
		const hasParameters = signature.parameters.length > 0;
		const activeParameterIndex = signature.activeParameter ?? hints.activeParameter;

J
Johannes Rieken 已提交
204
		if (!hasParameters) {
J
Joao Moreno 已提交
205 206 207
			const label = dom.append(code, $('span'));
			label.textContent = signature.label;
		} else {
208
			this.renderParameters(code, signature, activeParameterIndex);
E
Erich Gamma 已提交
209
		}
210

211 212
		const activeParameter: modes.ParameterInformation | undefined = signature.parameters[activeParameterIndex];
		if (activeParameter?.documentation) {
J
Joao Moreno 已提交
213
			const documentation = $('span.documentation');
214 215 216
			if (typeof activeParameter.documentation === 'string') {
				documentation.textContent = activeParameter.documentation;
			} else {
M
Matt Bierner 已提交
217
				const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(activeParameter.documentation));
218
				dom.addClass(renderedContents.element, 'markdown-docs');
219
				documentation.appendChild(renderedContents.element);
220
			}
221
			dom.append(this.domNodes.docs, $('p', {}, documentation));
J
Joao Moreno 已提交
222
		}
J
Joao Moreno 已提交
223

M
Matt Bierner 已提交
224 225 226
		if (signature.documentation === undefined) {
			/** no op */
		} else if (typeof signature.documentation === 'string') {
227
			dom.append(this.domNodes.docs, $('p', {}, signature.documentation));
228
		} else {
M
Matt Bierner 已提交
229
			const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(signature.documentation));
230
			dom.addClass(renderedContents.element, 'markdown-docs');
231
			dom.append(this.domNodes.docs, renderedContents.element);
J
Joao Moreno 已提交
232
		}
J
Joao Moreno 已提交
233

M
Matt Bierner 已提交
234
		const hasDocs = this.hasDocs(signature, activeParameter);
235

236 237
		dom.toggleClass(this.domNodes.signature, 'has-docs', hasDocs);
		dom.toggleClass(this.domNodes.docs, 'empty', !hasDocs);
238

239 240
		this.domNodes.overloads.textContent =
			pad(hints.activeSignature + 1, hints.signatures.length.toString().length) + '/' + hints.signatures.length;
J
Joao Moreno 已提交
241 242

		if (activeParameter) {
243
			const labelToAnnounce = this.getParameterLabel(signature, activeParameterIndex);
J
Joao Moreno 已提交
244 245 246 247 248 249 250
			// Select method gets called on every user type while parameter hints are visible.
			// We do not want to spam the user with same announcements, so we only announce if the current parameter changed.

			if (this.announcedLabel !== labelToAnnounce) {
				aria.alert(nls.localize('hint', "{0}, hint", labelToAnnounce));
				this.announcedLabel = labelToAnnounce;
			}
J
Joao Moreno 已提交
251
		}
E
Erich Gamma 已提交
252

J
Joao Moreno 已提交
253
		this.editor.layoutContentWidget(this);
254
		this.domNodes.scrollbar.scanDomNode();
255 256
	}

M
Matt Bierner 已提交
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
	private hasDocs(signature: modes.SignatureInformation, activeParameter: modes.ParameterInformation | undefined): boolean {
		if (activeParameter && typeof (activeParameter.documentation) === 'string' && activeParameter.documentation.length > 0) {
			return true;
		}
		if (activeParameter && typeof (activeParameter.documentation) === 'object' && activeParameter.documentation.value.length > 0) {
			return true;
		}
		if (typeof (signature.documentation) === 'string' && signature.documentation.length > 0) {
			return true;
		}
		if (typeof (signature.documentation) === 'object' && signature.documentation.value.length > 0) {
			return true;
		}
		return false;
	}

273 274
	private renderParameters(parent: HTMLElement, signature: modes.SignatureInformation, activeParameterIndex: number): void {
		const [start, end] = this.getParameterLabelOffsets(signature, activeParameterIndex);
E
Erich Gamma 已提交
275

M
Matt Bierner 已提交
276
		const beforeSpan = document.createElement('span');
277
		beforeSpan.textContent = signature.label.substring(0, start);
J
Joao Moreno 已提交
278

M
Matt Bierner 已提交
279
		const paramSpan = document.createElement('span');
280 281
		paramSpan.textContent = signature.label.substring(start, end);
		paramSpan.className = 'parameter active';
E
Erich Gamma 已提交
282

M
Matt Bierner 已提交
283
		const afterSpan = document.createElement('span');
284
		afterSpan.textContent = signature.label.substring(end);
J
Joao Moreno 已提交
285

286 287
		dom.append(parent, beforeSpan, paramSpan, afterSpan);
	}
J
Joao Moreno 已提交
288

289 290 291 292 293 294 295 296 297 298 299
	private getParameterLabel(signature: modes.SignatureInformation, paramIdx: number): string {
		const param = signature.parameters[paramIdx];
		if (typeof param.label === 'string') {
			return param.label;
		} else {
			return signature.label.substring(param.label[0], param.label[1]);
		}
	}

	private getParameterLabelOffsets(signature: modes.SignatureInformation, paramIdx: number): [number, number] {
		const param = signature.parameters[paramIdx];
J
Johannes Rieken 已提交
300 301 302
		if (!param) {
			return [0, 0];
		} else if (Array.isArray(param.label)) {
303 304 305 306 307 308
			return param.label;
		} else {
			const idx = signature.label.lastIndexOf(param.label);
			return idx >= 0
				? [idx, idx + param.label.length]
				: [0, 0];
E
Erich Gamma 已提交
309
		}
J
Joao Moreno 已提交
310 311
	}

312
	next(): void {
M
Matt Bierner 已提交
313 314
		this.editor.focus();
		this.model.next();
E
Erich Gamma 已提交
315 316
	}

317
	previous(): void {
M
Matt Bierner 已提交
318 319
		this.editor.focus();
		this.model.previous();
E
Erich Gamma 已提交
320 321
	}

J
Joao Moreno 已提交
322
	cancel(): void {
M
Matt Bierner 已提交
323
		this.model.cancel();
E
Erich Gamma 已提交
324 325
	}

J
Joao Moreno 已提交
326
	getDomNode(): HTMLElement {
327 328 329 330
		if (!this.domNodes) {
			this.createParamaterHintDOMNodes();
		}
		return this.domNodes!.element;
E
Erich Gamma 已提交
331 332
	}

J
Joao Moreno 已提交
333
	getId(): string {
E
Erich Gamma 已提交
334 335 336
		return ParameterHintsWidget.ID;
	}

337
	trigger(context: TriggerContext): void {
M
Matt Bierner 已提交
338
		this.model.trigger(context, 0);
J
Joao Moreno 已提交
339 340
	}

J
Joao Moreno 已提交
341
	private updateMaxHeight(): void {
342 343 344
		if (!this.domNodes) {
			return;
		}
J
Joao Moreno 已提交
345
		const height = Math.max(this.editor.getLayoutInfo().height / 4, 250);
346
		const maxHeight = `${height}px`;
347 348
		this.domNodes.element.style.maxHeight = maxHeight;
		const wrapper = this.domNodes.element.getElementsByClassName('wrapper') as HTMLCollectionOf<HTMLElement>;
349 350 351
		if (wrapper.length) {
			wrapper[0].style.maxHeight = maxHeight;
		}
J
Joao Moreno 已提交
352
	}
353 354 355
}

registerThemingParticipant((theme, collector) => {
M
Matt Bierner 已提交
356
	const border = theme.getColor(editorHoverBorder);
357
	if (border) {
M
Matt Bierner 已提交
358
		const borderWidth = theme.type === HIGH_CONTRAST ? 2 : 1;
359 360 361 362
		collector.addRule(`.monaco-editor .parameter-hints-widget { border: ${borderWidth}px solid ${border}; }`);
		collector.addRule(`.monaco-editor .parameter-hints-widget.multiple .body { border-left: 1px solid ${border.transparent(0.5)}; }`);
		collector.addRule(`.monaco-editor .parameter-hints-widget .signature.has-docs { border-bottom: 1px solid ${border.transparent(0.5)}; }`);
	}
M
Matt Bierner 已提交
363
	const background = theme.getColor(editorHoverBackground);
364 365 366
	if (background) {
		collector.addRule(`.monaco-editor .parameter-hints-widget { background-color: ${background}; }`);
	}
367 368 369 370 371

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

373 374 375 376 377
	const foreground = theme.getColor(editorHoverForeground);
	if (foreground) {
		collector.addRule(`.monaco-editor .parameter-hints-widget { color: ${foreground}; }`);
	}

M
Matt Bierner 已提交
378
	const codeBackground = theme.getColor(textCodeBlockBackground);
379 380 381
	if (codeBackground) {
		collector.addRule(`.monaco-editor .parameter-hints-widget code { background-color: ${codeBackground}; }`);
	}
382
});