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