Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/parameterHints/browser/parameterHintsModel.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 { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js';
7
import { onUnexpectedError } from '../../../../base/common/errors.js';
8
import { Emitter } from '../../../../base/common/event.js';
9
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
10
import { ICodeEditor } from '../../../browser/editorBrowser.js';
11
import { EditorOption } from '../../../common/config/editorOptions.js';
12
import { CharacterSet } from '../../../common/core/characterClassifier.js';
13
import { ICursorSelectionChangedEvent } from '../../../common/cursorEvents.js';
14
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
15
import * as languages from '../../../common/languages.js';
16
import { provideSignatureHelp } from './provideSignatureHelp.js';
17
18
export interface TriggerContext {
19
readonly triggerKind: languages.SignatureHelpTriggerKind;
20
readonly triggerCharacter?: string;
21
}
22
23
namespace ParameterHintState {
24
export const enum Type {
25
Default,
26
Active,
27
Pending,
28
}
29
30
export const Default = { type: Type.Default } as const;
31
32
export class Pending {
33
readonly type = Type.Pending;
34
constructor(
35
readonly request: CancelablePromise<languages.SignatureHelpResult | undefined | null>,
36
readonly previouslyActiveHints: languages.SignatureHelp | undefined,
37
) { }
38
}
39
40
export class Active {
41
readonly type = Type.Active;
42
constructor(
43
readonly hints: languages.SignatureHelp
44
) { }
45
}
46
47
export type State = typeof Default | Pending | Active;
48
}
49
50
export class ParameterHintsModel extends Disposable {
51
52
private static readonly DEFAULT_DELAY = 120; // ms
53
54
private readonly _onChangedHints = this._register(new Emitter<languages.SignatureHelp | undefined>());
55
public readonly onChangedHints = this._onChangedHints.event;
56
57
private readonly editor: ICodeEditor;
58
private readonly providers: LanguageFeatureRegistry<languages.SignatureHelpProvider>;
59
60
private triggerOnType = false;
61
private _state: ParameterHintState.State = ParameterHintState.Default;
62
private _pendingTriggers: TriggerContext[] = [];
63
64
private readonly _lastSignatureHelpResult = this._register(new MutableDisposable<languages.SignatureHelpResult>());
65
private readonly triggerChars = new CharacterSet();
66
private readonly retriggerChars = new CharacterSet();
67
68
private readonly throttledDelayer: Delayer<boolean>;
69
private triggerId = 0;
70
71
constructor(
72
editor: ICodeEditor,
73
providers: LanguageFeatureRegistry<languages.SignatureHelpProvider>,
74
delay: number = ParameterHintsModel.DEFAULT_DELAY
75
) {
76
super();
77
78
this.editor = editor;
79
this.providers = providers;
80
81
this.throttledDelayer = new Delayer(delay);
82
83
this._register(this.editor.onDidBlurEditorWidget(() => this.cancel()));
84
this._register(this.editor.onDidChangeConfiguration(() => this.onEditorConfigurationChange()));
85
this._register(this.editor.onDidChangeModel(e => this.onModelChanged()));
86
this._register(this.editor.onDidChangeModelLanguage(_ => this.onModelChanged()));
87
this._register(this.editor.onDidChangeCursorSelection(e => this.onCursorChange(e)));
88
this._register(this.editor.onDidChangeModelContent(e => this.onModelContentChange()));
89
this._register(this.providers.onDidChange(this.onModelChanged, this));
90
this._register(this.editor.onDidType(text => this.onDidType(text)));
91
92
this.onEditorConfigurationChange();
93
this.onModelChanged();
94
}
95
96
private get state() { return this._state; }
97
private set state(value: ParameterHintState.State) {
98
if (this._state.type === ParameterHintState.Type.Pending) {
99
this._state.request.cancel();
100
}
101
this._state = value;
102
}
103
104
cancel(silent: boolean = false): void {
105
this.state = ParameterHintState.Default;
106
107
this.throttledDelayer.cancel();
108
109
if (!silent) {
110
this._onChangedHints.fire(undefined);
111
}
112
}
113
114
trigger(context: TriggerContext, delay?: number): void {
115
const model = this.editor.getModel();
116
if (!model || !this.providers.has(model)) {
117
return;
118
}
119
120
const triggerId = ++this.triggerId;
121
122
this._pendingTriggers.push(context);
123
this.throttledDelayer.trigger(() => {
124
return this.doTrigger(triggerId);
125
}, delay)
126
.catch(onUnexpectedError);
127
}
128
129
public next(): void {
130
if (this.state.type !== ParameterHintState.Type.Active) {
131
return;
132
}
133
134
const length = this.state.hints.signatures.length;
135
const activeSignature = this.state.hints.activeSignature;
136
const last = (activeSignature % length) === (length - 1);
137
const cycle = this.editor.getOption(EditorOption.parameterHints).cycle;
138
139
// If there is only one signature, or we're on last signature of list
140
if ((length < 2 || last) && !cycle) {
141
this.cancel();
142
return;
143
}
144
145
this.updateActiveSignature(last && cycle ? 0 : activeSignature + 1);
146
}
147
148
public previous(): void {
149
if (this.state.type !== ParameterHintState.Type.Active) {
150
return;
151
}
152
153
const length = this.state.hints.signatures.length;
154
const activeSignature = this.state.hints.activeSignature;
155
const first = activeSignature === 0;
156
const cycle = this.editor.getOption(EditorOption.parameterHints).cycle;
157
158
// If there is only one signature, or we're on first signature of list
159
if ((length < 2 || first) && !cycle) {
160
this.cancel();
161
return;
162
}
163
164
this.updateActiveSignature(first && cycle ? length - 1 : activeSignature - 1);
165
}
166
167
private updateActiveSignature(activeSignature: number) {
168
if (this.state.type !== ParameterHintState.Type.Active) {
169
return;
170
}
171
172
this.state = new ParameterHintState.Active({ ...this.state.hints, activeSignature });
173
this._onChangedHints.fire(this.state.hints);
174
}
175
176
private async doTrigger(triggerId: number): Promise<boolean> {
177
const isRetrigger = this.state.type === ParameterHintState.Type.Active || this.state.type === ParameterHintState.Type.Pending;
178
const activeSignatureHelp = this.getLastActiveHints();
179
this.cancel(true);
180
181
if (this._pendingTriggers.length === 0) {
182
return false;
183
}
184
185
const context: TriggerContext = this._pendingTriggers.reduce(mergeTriggerContexts);
186
this._pendingTriggers = [];
187
188
const triggerContext = {
189
triggerKind: context.triggerKind,
190
triggerCharacter: context.triggerCharacter,
191
isRetrigger: isRetrigger,
192
activeSignatureHelp: activeSignatureHelp
193
};
194
195
if (!this.editor.hasModel()) {
196
return false;
197
}
198
199
const model = this.editor.getModel();
200
const position = this.editor.getPosition();
201
202
this.state = new ParameterHintState.Pending(
203
createCancelablePromise(token => provideSignatureHelp(this.providers, model, position, triggerContext, token)),
204
activeSignatureHelp);
205
206
try {
207
const result = await this.state.request;
208
209
// Check that we are still resolving the correct signature help
210
if (triggerId !== this.triggerId) {
211
result?.dispose();
212
213
return false;
214
}
215
216
if (!result || !result.value.signatures || result.value.signatures.length === 0) {
217
result?.dispose();
218
this._lastSignatureHelpResult.clear();
219
this.cancel();
220
return false;
221
} else {
222
this.state = new ParameterHintState.Active(result.value);
223
this._lastSignatureHelpResult.value = result;
224
this._onChangedHints.fire(this.state.hints);
225
return true;
226
}
227
} catch (error) {
228
if (triggerId === this.triggerId) {
229
this.state = ParameterHintState.Default;
230
}
231
onUnexpectedError(error);
232
return false;
233
}
234
}
235
236
private getLastActiveHints(): languages.SignatureHelp | undefined {
237
switch (this.state.type) {
238
case ParameterHintState.Type.Active: return this.state.hints;
239
case ParameterHintState.Type.Pending: return this.state.previouslyActiveHints;
240
default: return undefined;
241
}
242
}
243
244
private get isTriggered(): boolean {
245
return this.state.type === ParameterHintState.Type.Active
246
|| this.state.type === ParameterHintState.Type.Pending
247
|| this.throttledDelayer.isTriggered();
248
}
249
250
private onModelChanged(): void {
251
this.cancel();
252
253
this.triggerChars.clear();
254
this.retriggerChars.clear();
255
256
const model = this.editor.getModel();
257
if (!model) {
258
return;
259
}
260
261
for (const support of this.providers.ordered(model)) {
262
for (const ch of support.signatureHelpTriggerCharacters || []) {
263
if (ch.length) {
264
const charCode = ch.charCodeAt(0);
265
this.triggerChars.add(charCode);
266
267
// All trigger characters are also considered retrigger characters
268
this.retriggerChars.add(charCode);
269
}
270
}
271
272
for (const ch of support.signatureHelpRetriggerCharacters || []) {
273
if (ch.length) {
274
this.retriggerChars.add(ch.charCodeAt(0));
275
}
276
}
277
}
278
}
279
280
private onDidType(text: string) {
281
if (!this.triggerOnType) {
282
return;
283
}
284
285
const lastCharIndex = text.length - 1;
286
const triggerCharCode = text.charCodeAt(lastCharIndex);
287
288
if (this.triggerChars.has(triggerCharCode) || this.isTriggered && this.retriggerChars.has(triggerCharCode)) {
289
this.trigger({
290
triggerKind: languages.SignatureHelpTriggerKind.TriggerCharacter,
291
triggerCharacter: text.charAt(lastCharIndex),
292
});
293
}
294
}
295
296
private onCursorChange(e: ICursorSelectionChangedEvent): void {
297
if (e.source === 'mouse') {
298
this.cancel();
299
} else if (this.isTriggered) {
300
this.trigger({ triggerKind: languages.SignatureHelpTriggerKind.ContentChange });
301
}
302
}
303
304
private onModelContentChange(): void {
305
if (this.isTriggered) {
306
this.trigger({ triggerKind: languages.SignatureHelpTriggerKind.ContentChange });
307
}
308
}
309
310
private onEditorConfigurationChange(): void {
311
this.triggerOnType = this.editor.getOption(EditorOption.parameterHints).enabled;
312
313
if (!this.triggerOnType) {
314
this.cancel();
315
}
316
}
317
318
override dispose(): void {
319
this.cancel(true);
320
super.dispose();
321
}
322
}
323
324
function mergeTriggerContexts(previous: TriggerContext, current: TriggerContext) {
325
switch (current.triggerKind) {
326
case languages.SignatureHelpTriggerKind.Invoke:
327
// Invoke overrides previous triggers.
328
return current;
329
330
case languages.SignatureHelpTriggerKind.ContentChange:
331
// Ignore content changes triggers
332
return previous;
333
334
case languages.SignatureHelpTriggerKind.TriggerCharacter:
335
default:
336
return current;
337
}
338
}
339
340