Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts
5237 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import * as aria from '../../../../base/browser/ui/aria/aria.js';
8
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
9
import { Codicon } from '../../../../base/common/codicons.js';
10
import { Event } from '../../../../base/common/event.js';
11
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
12
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
13
import { escapeRegExpCharacters } from '../../../../base/common/strings.js';
14
import { assertReturnsDefined } from '../../../../base/common/types.js';
15
import './parameterHints.css';
16
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js';
17
import { EditorOption } from '../../../common/config/editorOptions.js';
18
import { EDITOR_FONT_DEFAULTS } from '../../../common/config/fontInfo.js';
19
import * as languages from '../../../common/languages.js';
20
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
21
import { IRenderedMarkdown } from '../../../../base/browser/markdownRenderer.js';
22
import { ParameterHintsModel } from './parameterHintsModel.js';
23
import { Context } from './provideSignatureHelp.js';
24
import * as nls from '../../../../nls.js';
25
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
26
import { listHighlightForeground, registerColor } from '../../../../platform/theme/common/colorRegistry.js';
27
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
28
import { ThemeIcon } from '../../../../base/common/themables.js';
29
30
const $ = dom.$;
31
32
const parameterHintsNextIcon = registerIcon('parameter-hints-next', Codicon.chevronDown, nls.localize('parameterHintsNextIcon', 'Icon for show next parameter hint.'));
33
const parameterHintsPreviousIcon = registerIcon('parameter-hints-previous', Codicon.chevronUp, nls.localize('parameterHintsPreviousIcon', 'Icon for show previous parameter hint.'));
34
35
export class ParameterHintsWidget extends Disposable implements IContentWidget {
36
37
private static readonly ID = 'editor.widget.parameterHintsWidget';
38
39
private readonly renderDisposeables = this._register(new DisposableStore());
40
private readonly keyVisible: IContextKey<boolean>;
41
private readonly keyMultipleSignatures: IContextKey<boolean>;
42
43
private domNodes?: {
44
readonly element: HTMLElement;
45
readonly signature: HTMLElement;
46
readonly docs: HTMLElement;
47
readonly overloads: HTMLElement;
48
readonly scrollbar: DomScrollableElement;
49
};
50
51
private visible: boolean = false;
52
private announcedLabel: string | null = null;
53
54
// Editor.IContentWidget.allowEditorOverflow
55
allowEditorOverflow = true;
56
57
constructor(
58
private readonly editor: ICodeEditor,
59
private readonly model: ParameterHintsModel,
60
@IContextKeyService contextKeyService: IContextKeyService,
61
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
62
) {
63
super();
64
65
this.keyVisible = Context.Visible.bindTo(contextKeyService);
66
this.keyMultipleSignatures = Context.MultipleSignatures.bindTo(contextKeyService);
67
}
68
69
private createParameterHintDOMNodes() {
70
const element = $('.editor-widget.parameter-hints-widget');
71
const wrapper = dom.append(element, $('.phwrapper'));
72
wrapper.tabIndex = -1;
73
74
const controls = dom.append(wrapper, $('.controls'));
75
const previous = dom.append(controls, $('.button' + ThemeIcon.asCSSSelector(parameterHintsPreviousIcon)));
76
const overloads = dom.append(controls, $('.overloads'));
77
const next = dom.append(controls, $('.button' + ThemeIcon.asCSSSelector(parameterHintsNextIcon)));
78
79
this._register(dom.addDisposableListener(previous, 'click', e => {
80
dom.EventHelper.stop(e);
81
this.previous();
82
}));
83
84
this._register(dom.addDisposableListener(next, 'click', e => {
85
dom.EventHelper.stop(e);
86
this.next();
87
}));
88
89
const body = $('.body');
90
const scrollbar = new DomScrollableElement(body, {
91
alwaysConsumeMouseWheel: true,
92
});
93
this._register(scrollbar);
94
wrapper.appendChild(scrollbar.getDomNode());
95
96
const signature = dom.append(body, $('.signature'));
97
const docs = dom.append(body, $('.docs'));
98
99
element.style.userSelect = 'text';
100
101
this.domNodes = {
102
element,
103
signature,
104
overloads,
105
docs,
106
scrollbar,
107
};
108
109
this.editor.addContentWidget(this);
110
this.hide();
111
112
this._register(this.editor.onDidChangeCursorSelection(e => {
113
if (this.visible) {
114
this.editor.layoutContentWidget(this);
115
}
116
}));
117
118
const updateFont = () => {
119
if (!this.domNodes) {
120
return;
121
}
122
123
const fontInfo = this.editor.getOption(EditorOption.fontInfo);
124
const element = this.domNodes.element;
125
element.style.fontSize = `${fontInfo.fontSize}px`;
126
element.style.lineHeight = `${fontInfo.lineHeight / fontInfo.fontSize}`;
127
element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamily', fontInfo.fontFamily);
128
element.style.setProperty('--vscode-parameterHintsWidget-editorFontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily);
129
};
130
131
updateFont();
132
133
this._register(Event.chain(
134
this.editor.onDidChangeConfiguration.bind(this.editor),
135
$ => $.filter(e => e.hasChanged(EditorOption.fontInfo))
136
)(updateFont));
137
138
this._register(this.editor.onDidLayoutChange(e => this.updateMaxHeight()));
139
this.updateMaxHeight();
140
}
141
142
public show(): void {
143
if (this.visible) {
144
return;
145
}
146
147
if (!this.domNodes) {
148
this.createParameterHintDOMNodes();
149
}
150
151
this.keyVisible.set(true);
152
this.visible = true;
153
setTimeout(() => {
154
this.domNodes?.element.classList.add('visible');
155
}, 100);
156
this.editor.layoutContentWidget(this);
157
}
158
159
public hide(): void {
160
this.renderDisposeables.clear();
161
162
if (!this.visible) {
163
return;
164
}
165
166
this.keyVisible.reset();
167
this.visible = false;
168
this.announcedLabel = null;
169
this.domNodes?.element.classList.remove('visible');
170
this.editor.layoutContentWidget(this);
171
}
172
173
getPosition(): IContentWidgetPosition | null {
174
if (this.visible) {
175
return {
176
position: this.editor.getPosition(),
177
preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]
178
};
179
}
180
return null;
181
}
182
183
public render(hints: languages.SignatureHelp): void {
184
this.renderDisposeables.clear();
185
186
if (!this.domNodes) {
187
return;
188
}
189
190
const multiple = hints.signatures.length > 1;
191
this.domNodes.element.classList.toggle('multiple', multiple);
192
this.keyMultipleSignatures.set(multiple);
193
194
this.domNodes.signature.innerText = '';
195
this.domNodes.docs.innerText = '';
196
197
const signature = hints.signatures[hints.activeSignature];
198
if (!signature) {
199
return;
200
}
201
202
const code = dom.append(this.domNodes.signature, $('.code'));
203
const hasParameters = signature.parameters.length > 0;
204
const activeParameterIndex = signature.activeParameter ?? hints.activeParameter;
205
206
if (!hasParameters) {
207
const label = dom.append(code, $('span'));
208
label.textContent = signature.label;
209
} else {
210
this.renderParameters(code, signature, activeParameterIndex);
211
}
212
213
const activeParameter: languages.ParameterInformation | undefined = signature.parameters[activeParameterIndex];
214
if (activeParameter?.documentation) {
215
const documentation = $('span.documentation');
216
if (typeof activeParameter.documentation === 'string') {
217
documentation.textContent = activeParameter.documentation;
218
} else {
219
const renderedContents = this.renderMarkdownDocs(activeParameter.documentation);
220
documentation.appendChild(renderedContents.element);
221
}
222
dom.append(this.domNodes.docs, $('p', {}, documentation));
223
}
224
225
if (signature.documentation === undefined) {
226
/** no op */
227
} else if (typeof signature.documentation === 'string') {
228
dom.append(this.domNodes.docs, $('p', {}, signature.documentation));
229
} else {
230
const renderedContents = this.renderMarkdownDocs(signature.documentation);
231
dom.append(this.domNodes.docs, renderedContents.element);
232
}
233
234
const hasDocs = this.hasDocs(signature, activeParameter);
235
236
this.domNodes.signature.classList.toggle('has-docs', hasDocs);
237
this.domNodes.docs.classList.toggle('empty', !hasDocs);
238
239
this.domNodes.overloads.textContent =
240
String(hints.activeSignature + 1).padStart(hints.signatures.length.toString().length, '0') + '/' + hints.signatures.length;
241
242
if (activeParameter) {
243
let labelToAnnounce = '';
244
const param = signature.parameters[activeParameterIndex];
245
if (Array.isArray(param.label)) {
246
labelToAnnounce = signature.label.substring(param.label[0], param.label[1]);
247
} else {
248
labelToAnnounce = param.label;
249
}
250
if (param.documentation) {
251
labelToAnnounce += typeof param.documentation === 'string' ? `, ${param.documentation}` : `, ${param.documentation.value}`;
252
}
253
if (signature.documentation) {
254
labelToAnnounce += typeof signature.documentation === 'string' ? `, ${signature.documentation}` : `, ${signature.documentation.value}`;
255
}
256
257
// Select method gets called on every user type while parameter hints are visible.
258
// We do not want to spam the user with same announcements, so we only announce if the current parameter changed.
259
260
if (this.announcedLabel !== labelToAnnounce) {
261
aria.alert(nls.localize('hint', "{0}, hint", labelToAnnounce));
262
this.announcedLabel = labelToAnnounce;
263
}
264
}
265
266
this.editor.layoutContentWidget(this);
267
this.domNodes.scrollbar.scanDomNode();
268
}
269
270
private renderMarkdownDocs(markdown: IMarkdownString): IRenderedMarkdown {
271
const renderedContents = this.renderDisposeables.add(this.markdownRendererService.render(markdown, {
272
context: this.editor,
273
asyncRenderCallback: () => {
274
this.domNodes?.scrollbar.scanDomNode();
275
}
276
}));
277
renderedContents.element.classList.add('markdown-docs');
278
return renderedContents;
279
}
280
281
private hasDocs(signature: languages.SignatureInformation, activeParameter: languages.ParameterInformation | undefined): boolean {
282
if (activeParameter && typeof activeParameter.documentation === 'string' && assertReturnsDefined(activeParameter.documentation).length > 0) {
283
return true;
284
}
285
if (activeParameter && typeof activeParameter.documentation === 'object' && assertReturnsDefined(activeParameter.documentation).value.length > 0) {
286
return true;
287
}
288
if (signature.documentation && typeof signature.documentation === 'string' && assertReturnsDefined(signature.documentation).length > 0) {
289
return true;
290
}
291
if (signature.documentation && typeof signature.documentation === 'object' && assertReturnsDefined(signature.documentation.value).length > 0) {
292
return true;
293
}
294
return false;
295
}
296
297
private renderParameters(parent: HTMLElement, signature: languages.SignatureInformation, activeParameterIndex: number): void {
298
const [start, end] = this.getParameterLabelOffsets(signature, activeParameterIndex);
299
300
const beforeSpan = document.createElement('span');
301
beforeSpan.textContent = signature.label.substring(0, start);
302
303
const paramSpan = document.createElement('span');
304
paramSpan.textContent = signature.label.substring(start, end);
305
paramSpan.className = 'parameter active';
306
307
const afterSpan = document.createElement('span');
308
afterSpan.textContent = signature.label.substring(end);
309
310
dom.append(parent, beforeSpan, paramSpan, afterSpan);
311
}
312
313
private getParameterLabelOffsets(signature: languages.SignatureInformation, paramIdx: number): [number, number] {
314
const param = signature.parameters[paramIdx];
315
if (!param) {
316
return [0, 0];
317
} else if (Array.isArray(param.label)) {
318
return param.label;
319
} else if (!param.label.length) {
320
return [0, 0];
321
} else {
322
const regex = new RegExp(`(\\W|^)${escapeRegExpCharacters(param.label)}(?=\\W|$)`, 'g');
323
regex.test(signature.label);
324
const idx = regex.lastIndex - param.label.length;
325
return idx >= 0
326
? [idx, regex.lastIndex]
327
: [0, 0];
328
}
329
}
330
331
next(): void {
332
this.editor.focus();
333
this.model.next();
334
}
335
336
previous(): void {
337
this.editor.focus();
338
this.model.previous();
339
}
340
341
getDomNode(): HTMLElement {
342
if (!this.domNodes) {
343
this.createParameterHintDOMNodes();
344
}
345
return this.domNodes!.element;
346
}
347
348
getId(): string {
349
return ParameterHintsWidget.ID;
350
}
351
352
private updateMaxHeight(): void {
353
if (!this.domNodes) {
354
return;
355
}
356
const height = Math.max(this.editor.getLayoutInfo().height / 4, 250);
357
const maxHeight = `${height}px`;
358
this.domNodes.element.style.maxHeight = maxHeight;
359
// eslint-disable-next-line no-restricted-syntax
360
const wrapper = this.domNodes.element.getElementsByClassName('phwrapper') as HTMLCollectionOf<HTMLElement>;
361
if (wrapper.length) {
362
wrapper[0].style.maxHeight = maxHeight;
363
}
364
}
365
}
366
367
registerColor('editorHoverWidget.highlightForeground', listHighlightForeground, nls.localize('editorHoverWidgetHighlightForeground', 'Foreground color of the active item in the parameter hint.'));
368
369