Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.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 assert from 'assert';
7
import { promiseWithResolvers } from '../../../../../base/common/async.js';
8
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
10
import { URI } from '../../../../../base/common/uri.js';
11
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
12
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
13
import { Position } from '../../../../common/core/position.js';
14
import { Handler } from '../../../../common/editorCommon.js';
15
import { LanguageFeatureRegistry } from '../../../../common/languageFeatureRegistry.js';
16
import * as languages from '../../../../common/languages.js';
17
import { ITextModel } from '../../../../common/model.js';
18
import { ParameterHintsModel } from '../../browser/parameterHintsModel.js';
19
import { createTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
20
import { createTextModel } from '../../../../test/common/testTextModel.js';
21
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
22
import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';
23
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
24
import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';
25
26
const mockFile = URI.parse('test:somefile.ttt');
27
const mockFileSelector = { scheme: 'test' };
28
29
30
const emptySigHelp: languages.SignatureHelp = {
31
signatures: [{
32
label: 'none',
33
parameters: []
34
}],
35
activeParameter: 0,
36
activeSignature: 0
37
};
38
39
const emptySigHelpResult: languages.SignatureHelpResult = {
40
value: emptySigHelp,
41
dispose: () => { }
42
};
43
44
suite('ParameterHintsModel', () => {
45
const disposables = new DisposableStore();
46
let registry: LanguageFeatureRegistry<languages.SignatureHelpProvider>;
47
48
setup(() => {
49
disposables.clear();
50
registry = new LanguageFeatureRegistry<languages.SignatureHelpProvider>();
51
});
52
53
teardown(() => {
54
disposables.clear();
55
});
56
57
ensureNoDisposablesAreLeakedInTestSuite();
58
59
function createMockEditor(fileContents: string) {
60
const textModel = disposables.add(createTextModel(fileContents, undefined, undefined, mockFile));
61
const editor = disposables.add(createTestCodeEditor(textModel, {
62
serviceCollection: new ServiceCollection(
63
[ITelemetryService, NullTelemetryService],
64
[IStorageService, disposables.add(new InMemoryStorageService())]
65
)
66
}));
67
return editor;
68
}
69
70
function getNextHint(model: ParameterHintsModel) {
71
return new Promise<languages.SignatureHelpResult | undefined>(resolve => {
72
const sub = disposables.add(model.onChangedHints(e => {
73
sub.dispose();
74
return resolve(e ? { value: e, dispose: () => { } } : undefined);
75
}));
76
});
77
}
78
79
test('Provider should get trigger character on type', async () => {
80
const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();
81
82
const triggerChar = '(';
83
84
const editor = createMockEditor('');
85
disposables.add(new ParameterHintsModel(editor, registry));
86
87
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
88
signatureHelpTriggerCharacters = [triggerChar];
89
signatureHelpRetriggerCharacters = [];
90
91
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext) {
92
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
93
assert.strictEqual(context.triggerCharacter, triggerChar);
94
done();
95
return undefined;
96
}
97
}));
98
99
await runWithFakedTimers({ useFakeTimers: true }, async () => {
100
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
101
await donePromise;
102
});
103
});
104
105
test('Provider should be retriggered if already active', async () => {
106
const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();
107
108
const triggerChar = '(';
109
110
const editor = createMockEditor('');
111
disposables.add(new ParameterHintsModel(editor, registry));
112
113
let invokeCount = 0;
114
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
115
signatureHelpTriggerCharacters = [triggerChar];
116
signatureHelpRetriggerCharacters = [];
117
118
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {
119
++invokeCount;
120
try {
121
if (invokeCount === 1) {
122
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
123
assert.strictEqual(context.triggerCharacter, triggerChar);
124
assert.strictEqual(context.isRetrigger, false);
125
assert.strictEqual(context.activeSignatureHelp, undefined);
126
127
// Retrigger
128
setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: triggerChar }), 0);
129
} else {
130
assert.strictEqual(invokeCount, 2);
131
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
132
assert.strictEqual(context.isRetrigger, true);
133
assert.strictEqual(context.triggerCharacter, triggerChar);
134
assert.strictEqual(context.activeSignatureHelp, emptySigHelp);
135
136
done();
137
}
138
return emptySigHelpResult;
139
} catch (err) {
140
console.error(err);
141
throw err;
142
}
143
}
144
}));
145
146
await runWithFakedTimers({ useFakeTimers: true }, async () => {
147
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
148
await donePromise;
149
});
150
});
151
152
test('Provider should not be retriggered if previous help is canceled first', async () => {
153
const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();
154
155
const triggerChar = '(';
156
157
const editor = createMockEditor('');
158
const hintModel = disposables.add(new ParameterHintsModel(editor, registry));
159
160
let invokeCount = 0;
161
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
162
signatureHelpTriggerCharacters = [triggerChar];
163
signatureHelpRetriggerCharacters = [];
164
165
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {
166
try {
167
++invokeCount;
168
if (invokeCount === 1) {
169
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
170
assert.strictEqual(context.triggerCharacter, triggerChar);
171
assert.strictEqual(context.isRetrigger, false);
172
assert.strictEqual(context.activeSignatureHelp, undefined);
173
174
// Cancel and retrigger
175
hintModel.cancel();
176
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
177
} else {
178
assert.strictEqual(invokeCount, 2);
179
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
180
assert.strictEqual(context.triggerCharacter, triggerChar);
181
assert.strictEqual(context.isRetrigger, true);
182
assert.strictEqual(context.activeSignatureHelp, undefined);
183
done();
184
}
185
return emptySigHelpResult;
186
} catch (err) {
187
console.error(err);
188
throw err;
189
}
190
}
191
}));
192
193
await runWithFakedTimers({ useFakeTimers: true }, () => {
194
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
195
return donePromise;
196
});
197
});
198
199
test('Provider should get last trigger character when triggered multiple times and only be invoked once', async () => {
200
const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();
201
202
const editor = createMockEditor('');
203
disposables.add(new ParameterHintsModel(editor, registry, 5));
204
205
let invokeCount = 0;
206
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
207
signatureHelpTriggerCharacters = ['a', 'b', 'c'];
208
signatureHelpRetriggerCharacters = [];
209
210
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext) {
211
try {
212
++invokeCount;
213
214
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
215
assert.strictEqual(context.isRetrigger, false);
216
assert.strictEqual(context.triggerCharacter, 'c');
217
218
// Give some time to allow for later triggers
219
setTimeout(() => {
220
assert.strictEqual(invokeCount, 1);
221
222
done();
223
}, 50);
224
return undefined;
225
} catch (err) {
226
console.error(err);
227
throw err;
228
}
229
}
230
}));
231
232
await runWithFakedTimers({ useFakeTimers: true }, async () => {
233
editor.trigger('keyboard', Handler.Type, { text: 'a' });
234
editor.trigger('keyboard', Handler.Type, { text: 'b' });
235
editor.trigger('keyboard', Handler.Type, { text: 'c' });
236
237
await donePromise;
238
});
239
});
240
241
test('Provider should be retriggered if already active', async () => {
242
const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();
243
244
const editor = createMockEditor('');
245
disposables.add(new ParameterHintsModel(editor, registry, 5));
246
247
let invokeCount = 0;
248
249
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
250
signatureHelpTriggerCharacters = ['a', 'b'];
251
signatureHelpRetriggerCharacters = [];
252
253
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {
254
try {
255
++invokeCount;
256
if (invokeCount === 1) {
257
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
258
assert.strictEqual(context.triggerCharacter, 'a');
259
260
// retrigger after delay for widget to show up
261
setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: 'b' }), 50);
262
} else if (invokeCount === 2) {
263
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
264
assert.ok(context.isRetrigger);
265
assert.strictEqual(context.triggerCharacter, 'b');
266
done();
267
} else {
268
assert.fail('Unexpected invoke');
269
}
270
271
return emptySigHelpResult;
272
} catch (err) {
273
console.error(err);
274
throw err;
275
}
276
}
277
}));
278
279
await runWithFakedTimers({ useFakeTimers: true }, () => {
280
editor.trigger('keyboard', Handler.Type, { text: 'a' });
281
return donePromise;
282
});
283
});
284
285
test('Should cancel existing request when new request comes in', async () => {
286
287
const editor = createMockEditor('abc def');
288
const hintsModel = disposables.add(new ParameterHintsModel(editor, registry));
289
290
let didRequestCancellationOf = -1;
291
let invokeCount = 0;
292
const longRunningProvider = new class implements languages.SignatureHelpProvider {
293
signatureHelpTriggerCharacters = [];
294
signatureHelpRetriggerCharacters = [];
295
296
297
provideSignatureHelp(_model: ITextModel, _position: Position, token: CancellationToken): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {
298
try {
299
const count = invokeCount++;
300
disposables.add(token.onCancellationRequested(() => { didRequestCancellationOf = count; }));
301
302
// retrigger on first request
303
if (count === 0) {
304
hintsModel.trigger({ triggerKind: languages.SignatureHelpTriggerKind.Invoke }, 0);
305
}
306
307
return new Promise<languages.SignatureHelpResult>(resolve => {
308
setTimeout(() => {
309
resolve({
310
value: {
311
signatures: [{
312
label: '' + count,
313
parameters: []
314
}],
315
activeParameter: 0,
316
activeSignature: 0
317
},
318
dispose: () => { }
319
});
320
}, 100);
321
});
322
} catch (err) {
323
console.error(err);
324
throw err;
325
}
326
}
327
};
328
329
disposables.add(registry.register(mockFileSelector, longRunningProvider));
330
331
await runWithFakedTimers({ useFakeTimers: true }, async () => {
332
333
hintsModel.trigger({ triggerKind: languages.SignatureHelpTriggerKind.Invoke }, 0);
334
assert.strictEqual(-1, didRequestCancellationOf);
335
336
return new Promise<void>((resolve, reject) =>
337
disposables.add(hintsModel.onChangedHints(newParamterHints => {
338
try {
339
assert.strictEqual(0, didRequestCancellationOf);
340
assert.strictEqual('1', newParamterHints!.signatures[0].label);
341
resolve();
342
} catch (e) {
343
reject(e);
344
}
345
})));
346
});
347
});
348
349
test('Provider should be retriggered by retrigger character', async () => {
350
const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();
351
352
const triggerChar = 'a';
353
const retriggerChar = 'b';
354
355
const editor = createMockEditor('');
356
disposables.add(new ParameterHintsModel(editor, registry, 5));
357
358
let invokeCount = 0;
359
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
360
signatureHelpTriggerCharacters = [triggerChar];
361
signatureHelpRetriggerCharacters = [retriggerChar];
362
363
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {
364
try {
365
++invokeCount;
366
if (invokeCount === 1) {
367
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
368
assert.strictEqual(context.triggerCharacter, triggerChar);
369
370
// retrigger after delay for widget to show up
371
setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: retriggerChar }), 50);
372
} else if (invokeCount === 2) {
373
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
374
assert.ok(context.isRetrigger);
375
assert.strictEqual(context.triggerCharacter, retriggerChar);
376
done();
377
} else {
378
assert.fail('Unexpected invoke');
379
}
380
381
return emptySigHelpResult;
382
} catch (err) {
383
console.error(err);
384
throw err;
385
}
386
}
387
}));
388
389
await runWithFakedTimers({ useFakeTimers: true }, async () => {
390
// This should not trigger anything
391
editor.trigger('keyboard', Handler.Type, { text: retriggerChar });
392
393
// But a trigger character should
394
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
395
396
return donePromise;
397
});
398
});
399
400
test('should use first result from multiple providers', async () => {
401
const triggerChar = 'a';
402
const firstProviderId = 'firstProvider';
403
const secondProviderId = 'secondProvider';
404
const paramterLabel = 'parameter';
405
406
const editor = createMockEditor('');
407
const model = disposables.add(new ParameterHintsModel(editor, registry, 5));
408
409
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
410
signatureHelpTriggerCharacters = [triggerChar];
411
signatureHelpRetriggerCharacters = [];
412
413
async provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): Promise<languages.SignatureHelpResult | undefined> {
414
try {
415
if (!context.isRetrigger) {
416
// retrigger after delay for widget to show up
417
setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: triggerChar }), 50);
418
419
return {
420
value: {
421
activeParameter: 0,
422
activeSignature: 0,
423
signatures: [{
424
label: firstProviderId,
425
parameters: [
426
{ label: paramterLabel }
427
]
428
}]
429
},
430
dispose: () => { }
431
};
432
}
433
434
return undefined;
435
} catch (err) {
436
console.error(err);
437
throw err;
438
}
439
}
440
}));
441
442
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
443
signatureHelpTriggerCharacters = [triggerChar];
444
signatureHelpRetriggerCharacters = [];
445
446
async provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): Promise<languages.SignatureHelpResult | undefined> {
447
if (context.isRetrigger) {
448
return {
449
value: {
450
activeParameter: 0,
451
activeSignature: context.activeSignatureHelp ? context.activeSignatureHelp.activeSignature + 1 : 0,
452
signatures: [{
453
label: secondProviderId,
454
parameters: context.activeSignatureHelp ? context.activeSignatureHelp.signatures[0].parameters : []
455
}]
456
},
457
dispose: () => { }
458
};
459
}
460
461
return undefined;
462
}
463
}));
464
465
await runWithFakedTimers({ useFakeTimers: true }, async () => {
466
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
467
468
const firstHint = (await getNextHint(model))!.value;
469
assert.strictEqual(firstHint.signatures[0].label, firstProviderId);
470
assert.strictEqual(firstHint.activeSignature, 0);
471
assert.strictEqual(firstHint.signatures[0].parameters[0].label, paramterLabel);
472
473
const secondHint = (await getNextHint(model))!.value;
474
assert.strictEqual(secondHint.signatures[0].label, secondProviderId);
475
assert.strictEqual(secondHint.activeSignature, 1);
476
assert.strictEqual(secondHint.signatures[0].parameters[0].label, paramterLabel);
477
});
478
});
479
480
test('Quick typing should use the first trigger character', async () => {
481
const editor = createMockEditor('');
482
const model = disposables.add(new ParameterHintsModel(editor, registry, 50));
483
484
const triggerCharacter = 'a';
485
486
let invokeCount = 0;
487
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
488
signatureHelpTriggerCharacters = [triggerCharacter];
489
signatureHelpRetriggerCharacters = [];
490
491
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {
492
try {
493
++invokeCount;
494
495
if (invokeCount === 1) {
496
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
497
assert.strictEqual(context.triggerCharacter, triggerCharacter);
498
} else {
499
assert.fail('Unexpected invoke');
500
}
501
502
return emptySigHelpResult;
503
} catch (err) {
504
console.error(err);
505
throw err;
506
}
507
}
508
}));
509
510
await runWithFakedTimers({ useFakeTimers: true }, async () => {
511
editor.trigger('keyboard', Handler.Type, { text: triggerCharacter });
512
editor.trigger('keyboard', Handler.Type, { text: 'x' });
513
514
await getNextHint(model);
515
});
516
});
517
518
test('Retrigger while a pending resolve is still going on should preserve last active signature #96702', async () => {
519
const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();
520
521
const editor = createMockEditor('');
522
const model = disposables.add(new ParameterHintsModel(editor, registry, 50));
523
524
const triggerCharacter = 'a';
525
const retriggerCharacter = 'b';
526
527
let invokeCount = 0;
528
disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {
529
signatureHelpTriggerCharacters = [triggerCharacter];
530
signatureHelpRetriggerCharacters = [retriggerCharacter];
531
532
async provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): Promise<languages.SignatureHelpResult> {
533
try {
534
++invokeCount;
535
536
if (invokeCount === 1) {
537
assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);
538
assert.strictEqual(context.triggerCharacter, triggerCharacter);
539
setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: retriggerCharacter }), 50);
540
} else if (invokeCount === 2) {
541
// Trigger again while we wait for resolve to take place
542
setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: retriggerCharacter }), 50);
543
await new Promise(resolve => setTimeout(resolve, 1000));
544
} else if (invokeCount === 3) {
545
// Make sure that in a retrigger during a pending resolve, we still have the old active signature.
546
assert.strictEqual(context.activeSignatureHelp, emptySigHelp);
547
done();
548
} else {
549
assert.fail('Unexpected invoke');
550
}
551
552
return emptySigHelpResult;
553
} catch (err) {
554
console.error(err);
555
done(err);
556
throw err;
557
}
558
}
559
}));
560
561
await runWithFakedTimers({ useFakeTimers: true }, async () => {
562
563
editor.trigger('keyboard', Handler.Type, { text: triggerCharacter });
564
565
await getNextHint(model);
566
await getNextHint(model);
567
568
await donePromise;
569
});
570
});
571
});
572
573