Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts
4798 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
import assert from 'assert';
6
import { Event } from '../../../../../base/common/event.js';
7
import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { mock } from '../../../../../base/test/common/mock.js';
10
import { CoreEditingCommands } from '../../../../browser/coreCommands.js';
11
import { EditOperation } from '../../../../common/core/editOperation.js';
12
import { Position } from '../../../../common/core/position.js';
13
import { Range } from '../../../../common/core/range.js';
14
import { Selection } from '../../../../common/core/selection.js';
15
import { Handler } from '../../../../common/editorCommon.js';
16
import { ITextModel } from '../../../../common/model.js';
17
import { TextModel } from '../../../../common/model/textModel.js';
18
import { CompletionItemKind, CompletionItemProvider, CompletionList, CompletionTriggerKind, EncodedTokenizationResult, IState, TokenizationRegistry } from '../../../../common/languages.js';
19
import { MetadataConsts } from '../../../../common/encodedTokenAttributes.js';
20
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
21
import { NullState } from '../../../../common/languages/nullTokenize.js';
22
import { ILanguageService } from '../../../../common/languages/language.js';
23
import { SnippetController2 } from '../../../snippet/browser/snippetController2.js';
24
import { SuggestController } from '../../browser/suggestController.js';
25
import { ISuggestMemoryService } from '../../browser/suggestMemory.js';
26
import { LineContext, SuggestModel } from '../../browser/suggestModel.js';
27
import { ISelectedSuggestion } from '../../browser/suggestWidget.js';
28
import { createTestCodeEditor, ITestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
29
import { createModelServices, createTextModel, instantiateTextModel } from '../../../../test/common/testTextModel.js';
30
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
31
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
32
import { MockKeybindingService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
33
import { ILabelService } from '../../../../../platform/label/common/label.js';
34
import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';
35
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
36
import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';
37
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
38
import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';
39
import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';
40
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
41
import { getSnippetSuggestSupport, setSnippetSuggestSupport } from '../../browser/suggest.js';
42
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
43
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
44
45
46
function createMockEditor(model: TextModel, languageFeaturesService: ILanguageFeaturesService): ITestCodeEditor {
47
48
const storeService = new InMemoryStorageService();
49
const editor = createTestCodeEditor(model, {
50
serviceCollection: new ServiceCollection(
51
[ILanguageFeaturesService, languageFeaturesService],
52
[ITelemetryService, NullTelemetryService],
53
[IStorageService, storeService],
54
[IKeybindingService, new MockKeybindingService()],
55
[ISuggestMemoryService, new class implements ISuggestMemoryService {
56
declare readonly _serviceBrand: undefined;
57
memorize(): void {
58
}
59
select(): number {
60
return -1;
61
}
62
}],
63
[ILabelService, new class extends mock<ILabelService>() { }],
64
[IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { }],
65
[IEnvironmentService, new class extends mock<IEnvironmentService>() {
66
override isBuilt: boolean = true;
67
override isExtensionDevelopment: boolean = false;
68
}],
69
),
70
});
71
const ctrl = editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);
72
editor.hasWidgetFocus = () => true;
73
74
editor.registerDisposable(ctrl);
75
editor.registerDisposable(storeService);
76
return editor;
77
}
78
79
suite('SuggestModel - Context', function () {
80
const OUTER_LANGUAGE_ID = 'outerMode';
81
const INNER_LANGUAGE_ID = 'innerMode';
82
83
class OuterMode extends Disposable {
84
public readonly languageId = OUTER_LANGUAGE_ID;
85
constructor(
86
@ILanguageService languageService: ILanguageService,
87
@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService,
88
) {
89
super();
90
this._register(languageService.registerLanguage({ id: this.languageId }));
91
this._register(languageConfigurationService.register(this.languageId, {}));
92
93
this._register(TokenizationRegistry.register(this.languageId, {
94
getInitialState: (): IState => NullState,
95
tokenize: undefined!,
96
tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {
97
const tokensArr: number[] = [];
98
let prevLanguageId: string | undefined = undefined;
99
for (let i = 0; i < line.length; i++) {
100
const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID);
101
const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId);
102
if (prevLanguageId !== languageId) {
103
tokensArr.push(i);
104
tokensArr.push((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET));
105
}
106
prevLanguageId = languageId;
107
}
108
109
const tokens = new Uint32Array(tokensArr.length);
110
for (let i = 0; i < tokens.length; i++) {
111
tokens[i] = tokensArr[i];
112
}
113
return new EncodedTokenizationResult(tokens, [], state);
114
}
115
}));
116
}
117
}
118
119
class InnerMode extends Disposable {
120
public readonly languageId = INNER_LANGUAGE_ID;
121
constructor(
122
@ILanguageService languageService: ILanguageService,
123
@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService
124
) {
125
super();
126
this._register(languageService.registerLanguage({ id: this.languageId }));
127
this._register(languageConfigurationService.register(this.languageId, {}));
128
}
129
}
130
131
const assertAutoTrigger = (model: TextModel, offset: number, expected: boolean, message?: string): void => {
132
const pos = model.getPositionAt(offset);
133
const editor = createMockEditor(model, new LanguageFeaturesService());
134
editor.setPosition(pos);
135
assert.strictEqual(LineContext.shouldAutoTrigger(editor), expected, message);
136
editor.dispose();
137
};
138
139
let disposables: DisposableStore;
140
141
setup(() => {
142
disposables = new DisposableStore();
143
});
144
145
teardown(function () {
146
disposables.dispose();
147
});
148
149
ensureNoDisposablesAreLeakedInTestSuite();
150
151
test('Context - shouldAutoTrigger', function () {
152
const model = createTextModel('Das Pferd frisst keinen Gurkensalat - Philipp Reis 1861.\nWer hat\'s erfunden?');
153
disposables.add(model);
154
155
assertAutoTrigger(model, 3, true, 'end of word, Das|');
156
assertAutoTrigger(model, 4, false, 'no word Das |');
157
assertAutoTrigger(model, 1, true, 'typing a single character before a word: D|as');
158
assertAutoTrigger(model, 55, false, 'number, 1861|');
159
model.dispose();
160
});
161
162
test('shouldAutoTrigger at embedded language boundaries', () => {
163
const disposables = new DisposableStore();
164
const instantiationService = createModelServices(disposables);
165
const outerMode = disposables.add(instantiationService.createInstance(OuterMode));
166
disposables.add(instantiationService.createInstance(InnerMode));
167
168
const model = disposables.add(instantiateTextModel(instantiationService, 'a<xx>a<x>', outerMode.languageId));
169
170
assertAutoTrigger(model, 1, true, 'a|<x — should trigger at end of word');
171
assertAutoTrigger(model, 2, false, 'a<|x — should NOT trigger at start of word');
172
assertAutoTrigger(model, 3, true, 'a<x|x — should trigger after typing a single character before a word');
173
assertAutoTrigger(model, 4, true, 'a<xx|> — should trigger at boundary between languages');
174
assertAutoTrigger(model, 5, false, 'a<xx>|a — should NOT trigger at start of word');
175
assertAutoTrigger(model, 6, true, 'a<xx>a|< — should trigger at end of word');
176
assertAutoTrigger(model, 8, true, 'a<xx>a<x|> — should trigger at end of word at boundary');
177
178
disposables.dispose();
179
});
180
});
181
182
suite('SuggestModel - TriggerAndCancelOracle', function () {
183
184
185
function getDefaultSuggestRange(model: ITextModel, position: Position) {
186
const wordUntil = model.getWordUntilPosition(position);
187
return new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);
188
}
189
190
const alwaysEmptySupport: CompletionItemProvider = {
191
_debugDisplayName: 'test',
192
provideCompletionItems(doc, pos): CompletionList {
193
return {
194
incomplete: false,
195
suggestions: []
196
};
197
}
198
};
199
200
const alwaysSomethingSupport: CompletionItemProvider = {
201
_debugDisplayName: 'test',
202
provideCompletionItems(doc, pos): CompletionList {
203
return {
204
incomplete: false,
205
suggestions: [{
206
label: doc.getWordUntilPosition(pos).word,
207
kind: CompletionItemKind.Property,
208
insertText: 'foofoo',
209
range: getDefaultSuggestRange(doc, pos)
210
}]
211
};
212
}
213
};
214
215
let disposables: DisposableStore;
216
let model: TextModel;
217
const languageFeaturesService = new LanguageFeaturesService();
218
const registry = languageFeaturesService.completionProvider;
219
220
setup(function () {
221
disposables = new DisposableStore();
222
model = createTextModel('abc def', undefined, undefined, URI.parse('test:somefile.ttt'));
223
disposables.add(model);
224
});
225
226
teardown(() => {
227
disposables.dispose();
228
});
229
230
ensureNoDisposablesAreLeakedInTestSuite();
231
232
function withOracle(callback: (model: SuggestModel, editor: ITestCodeEditor) => any): Promise<any> {
233
234
return new Promise((resolve, reject) => {
235
const editor = createMockEditor(model, languageFeaturesService);
236
const oracle = editor.invokeWithinContext(accessor => accessor.get(IInstantiationService).createInstance(SuggestModel, editor));
237
disposables.add(oracle);
238
disposables.add(editor);
239
240
try {
241
resolve(callback(oracle, editor));
242
} catch (err) {
243
reject(err);
244
}
245
});
246
}
247
248
function assertEvent<E>(event: Event<E>, action: () => any, assert: (e: E) => any) {
249
return new Promise((resolve, reject) => {
250
const sub = event(e => {
251
sub.dispose();
252
try {
253
resolve(assert(e));
254
} catch (err) {
255
reject(err);
256
}
257
});
258
try {
259
action();
260
} catch (err) {
261
sub.dispose();
262
reject(err);
263
}
264
});
265
}
266
267
test('events - cancel/trigger', function () {
268
return withOracle(model => {
269
270
return Promise.all([
271
272
assertEvent(model.onDidTrigger, function () {
273
model.trigger({ auto: true });
274
}, function (event) {
275
assert.strictEqual(event.auto, true);
276
277
return assertEvent(model.onDidCancel, function () {
278
model.cancel();
279
}, function (event) {
280
assert.strictEqual(event.retrigger, false);
281
});
282
}),
283
284
assertEvent(model.onDidTrigger, function () {
285
model.trigger({ auto: true });
286
}, function (event) {
287
assert.strictEqual(event.auto, true);
288
}),
289
290
assertEvent(model.onDidTrigger, function () {
291
model.trigger({ auto: false });
292
}, function (event) {
293
assert.strictEqual(event.auto, false);
294
})
295
]);
296
});
297
});
298
299
300
test('events - suggest/empty', function () {
301
302
disposables.add(registry.register({ scheme: 'test' }, alwaysEmptySupport));
303
304
return withOracle(model => {
305
return Promise.all([
306
assertEvent(model.onDidCancel, function () {
307
model.trigger({ auto: true });
308
}, function (event) {
309
assert.strictEqual(event.retrigger, false);
310
}),
311
assertEvent(model.onDidSuggest, function () {
312
model.trigger({ auto: false });
313
}, function (event) {
314
assert.strictEqual(event.triggerOptions.auto, false);
315
assert.strictEqual(event.isFrozen, false);
316
assert.strictEqual(event.completionModel.items.length, 0);
317
})
318
]);
319
});
320
});
321
322
test('trigger - on type', function () {
323
324
disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));
325
326
return withOracle((model, editor) => {
327
return assertEvent(model.onDidSuggest, () => {
328
editor.setPosition({ lineNumber: 1, column: 4 });
329
editor.trigger('keyboard', Handler.Type, { text: 'd' });
330
331
}, event => {
332
assert.strictEqual(event.triggerOptions.auto, true);
333
assert.strictEqual(event.completionModel.items.length, 1);
334
const [first] = event.completionModel.items;
335
336
assert.strictEqual(first.provider, alwaysSomethingSupport);
337
});
338
});
339
});
340
341
test('#17400: Keep filtering suggestModel.ts after space', function () {
342
343
disposables.add(registry.register({ scheme: 'test' }, {
344
_debugDisplayName: 'test',
345
provideCompletionItems(doc, pos): CompletionList {
346
return {
347
incomplete: false,
348
suggestions: [{
349
label: 'My Table',
350
kind: CompletionItemKind.Property,
351
insertText: 'My Table',
352
range: getDefaultSuggestRange(doc, pos)
353
}]
354
};
355
}
356
}));
357
358
model.setValue('');
359
360
return withOracle((model, editor) => {
361
362
return assertEvent(model.onDidSuggest, () => {
363
// make sure completionModel starts here!
364
model.trigger({ auto: true });
365
}, event => {
366
367
return assertEvent(model.onDidSuggest, () => {
368
editor.setPosition({ lineNumber: 1, column: 1 });
369
editor.trigger('keyboard', Handler.Type, { text: 'My' });
370
371
}, event => {
372
assert.strictEqual(event.triggerOptions.auto, true);
373
assert.strictEqual(event.completionModel.items.length, 1);
374
const [first] = event.completionModel.items;
375
assert.strictEqual(first.completion.label, 'My Table');
376
377
return assertEvent(model.onDidSuggest, () => {
378
editor.setPosition({ lineNumber: 1, column: 3 });
379
editor.trigger('keyboard', Handler.Type, { text: ' ' });
380
381
}, event => {
382
assert.strictEqual(event.triggerOptions.auto, true);
383
assert.strictEqual(event.completionModel.items.length, 1);
384
const [first] = event.completionModel.items;
385
assert.strictEqual(first.completion.label, 'My Table');
386
});
387
});
388
});
389
});
390
});
391
392
test('#21484: Trigger character always force a new completion session', function () {
393
394
disposables.add(registry.register({ scheme: 'test' }, {
395
_debugDisplayName: 'test',
396
provideCompletionItems(doc, pos): CompletionList {
397
return {
398
incomplete: false,
399
suggestions: [{
400
label: 'foo.bar',
401
kind: CompletionItemKind.Property,
402
insertText: 'foo.bar',
403
range: Range.fromPositions(pos.with(undefined, 1), pos)
404
}]
405
};
406
}
407
}));
408
409
disposables.add(registry.register({ scheme: 'test' }, {
410
_debugDisplayName: 'test',
411
triggerCharacters: ['.'],
412
provideCompletionItems(doc, pos): CompletionList {
413
return {
414
incomplete: false,
415
suggestions: [{
416
label: 'boom',
417
kind: CompletionItemKind.Property,
418
insertText: 'boom',
419
range: Range.fromPositions(
420
pos.delta(0, doc.getLineContent(pos.lineNumber)[pos.column - 2] === '.' ? 0 : -1),
421
pos
422
)
423
}]
424
};
425
}
426
}));
427
428
model.setValue('');
429
430
return withOracle(async (model, editor) => {
431
432
await assertEvent(model.onDidSuggest, () => {
433
editor.setPosition({ lineNumber: 1, column: 1 });
434
editor.trigger('keyboard', Handler.Type, { text: 'foo' });
435
436
}, event => {
437
assert.strictEqual(event.triggerOptions.auto, true);
438
assert.strictEqual(event.completionModel.items.length, 1);
439
const [first] = event.completionModel.items;
440
assert.strictEqual(first.completion.label, 'foo.bar');
441
442
});
443
444
await assertEvent(model.onDidSuggest, () => {
445
editor.trigger('keyboard', Handler.Type, { text: '.' });
446
447
}, event => {
448
// SYNC
449
assert.strictEqual(event.triggerOptions.auto, true);
450
assert.strictEqual(event.completionModel.items.length, 1);
451
const [first] = event.completionModel.items;
452
assert.strictEqual(first.completion.label, 'foo.bar');
453
});
454
455
await assertEvent(model.onDidSuggest, () => {
456
// nothing -> triggered by the trigger character typing (see above)
457
458
}, event => {
459
// ASYNC
460
assert.strictEqual(event.triggerOptions.auto, true);
461
assert.strictEqual(event.completionModel.items.length, 2);
462
const [first, second] = event.completionModel.items;
463
assert.strictEqual(first.completion.label, 'foo.bar');
464
assert.strictEqual(second.completion.label, 'boom');
465
});
466
});
467
});
468
469
test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [1/2]', function () {
470
471
disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));
472
473
return withOracle((model, editor) => {
474
475
editor.getModel()!.setValue('fo');
476
editor.setPosition({ lineNumber: 1, column: 3 });
477
478
return assertEvent(model.onDidSuggest, () => {
479
model.trigger({ auto: false });
480
}, event => {
481
assert.strictEqual(event.triggerOptions.auto, false);
482
assert.strictEqual(event.isFrozen, false);
483
assert.strictEqual(event.completionModel.items.length, 1);
484
485
return assertEvent(model.onDidCancel, () => {
486
editor.trigger('keyboard', Handler.Type, { text: '+' });
487
}, event => {
488
assert.strictEqual(event.retrigger, false);
489
});
490
});
491
});
492
});
493
494
test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [2/2]', function () {
495
496
disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));
497
498
return withOracle((model, editor) => {
499
500
editor.getModel()!.setValue('fo');
501
editor.setPosition({ lineNumber: 1, column: 3 });
502
503
return assertEvent(model.onDidSuggest, () => {
504
model.trigger({ auto: false });
505
}, event => {
506
assert.strictEqual(event.triggerOptions.auto, false);
507
assert.strictEqual(event.isFrozen, false);
508
assert.strictEqual(event.completionModel.items.length, 1);
509
510
return assertEvent(model.onDidCancel, () => {
511
editor.trigger('keyboard', Handler.Type, { text: ' ' });
512
}, event => {
513
assert.strictEqual(event.retrigger, false);
514
});
515
});
516
});
517
});
518
519
test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (1/2)', function () {
520
521
disposables.add(registry.register({ scheme: 'test' }, {
522
_debugDisplayName: 'test',
523
provideCompletionItems(doc, pos): CompletionList {
524
return {
525
incomplete: true,
526
suggestions: [{
527
label: 'foo',
528
kind: CompletionItemKind.Property,
529
insertText: 'foo',
530
range: Range.fromPositions(pos.with(undefined, 1), pos)
531
}]
532
};
533
}
534
}));
535
536
return withOracle((model, editor) => {
537
538
editor.getModel()!.setValue('foo');
539
editor.setPosition({ lineNumber: 1, column: 4 });
540
541
return assertEvent(model.onDidSuggest, () => {
542
model.trigger({ auto: false });
543
}, event => {
544
assert.strictEqual(event.triggerOptions.auto, false);
545
assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);
546
assert.strictEqual(event.completionModel.items.length, 1);
547
548
return assertEvent(model.onDidCancel, () => {
549
editor.trigger('keyboard', Handler.Type, { text: ';' });
550
}, event => {
551
assert.strictEqual(event.retrigger, false);
552
});
553
});
554
});
555
});
556
557
test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (2/2)', function () {
558
559
disposables.add(registry.register({ scheme: 'test' }, {
560
_debugDisplayName: 'test',
561
provideCompletionItems(doc, pos): CompletionList {
562
return {
563
incomplete: true,
564
suggestions: [{
565
label: 'foo;',
566
kind: CompletionItemKind.Property,
567
insertText: 'foo',
568
range: Range.fromPositions(pos.with(undefined, 1), pos)
569
}]
570
};
571
}
572
}));
573
574
return withOracle((model, editor) => {
575
576
editor.getModel()!.setValue('foo');
577
editor.setPosition({ lineNumber: 1, column: 4 });
578
579
return assertEvent(model.onDidSuggest, () => {
580
model.trigger({ auto: false });
581
}, event => {
582
assert.strictEqual(event.triggerOptions.auto, false);
583
assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);
584
assert.strictEqual(event.completionModel.items.length, 1);
585
586
return assertEvent(model.onDidSuggest, () => {
587
// while we cancel incrementally enriching the set of
588
// completions we still filter against those that we have
589
// until now
590
editor.trigger('keyboard', Handler.Type, { text: ';' });
591
}, event => {
592
assert.strictEqual(event.triggerOptions.auto, false);
593
assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);
594
assert.strictEqual(event.completionModel.items.length, 1);
595
596
});
597
});
598
});
599
});
600
601
test('Trigger character is provided in suggest context', function () {
602
let triggerCharacter = '';
603
disposables.add(registry.register({ scheme: 'test' }, {
604
_debugDisplayName: 'test',
605
triggerCharacters: ['.'],
606
provideCompletionItems(doc, pos, context): CompletionList {
607
assert.strictEqual(context.triggerKind, CompletionTriggerKind.TriggerCharacter);
608
triggerCharacter = context.triggerCharacter!;
609
return {
610
incomplete: false,
611
suggestions: [
612
{
613
label: 'foo.bar',
614
kind: CompletionItemKind.Property,
615
insertText: 'foo.bar',
616
range: Range.fromPositions(pos.with(undefined, 1), pos)
617
}
618
]
619
};
620
}
621
}));
622
623
model.setValue('');
624
625
return withOracle((model, editor) => {
626
627
return assertEvent(model.onDidSuggest, () => {
628
editor.setPosition({ lineNumber: 1, column: 1 });
629
editor.trigger('keyboard', Handler.Type, { text: 'foo.' });
630
}, event => {
631
assert.strictEqual(triggerCharacter, '.');
632
});
633
});
634
});
635
636
test('Mac press and hold accent character insertion does not update suggestions, #35269', function () {
637
disposables.add(registry.register({ scheme: 'test' }, {
638
_debugDisplayName: 'test',
639
provideCompletionItems(doc, pos): CompletionList {
640
return {
641
incomplete: true,
642
suggestions: [{
643
label: 'abc',
644
kind: CompletionItemKind.Property,
645
insertText: 'abc',
646
range: Range.fromPositions(pos.with(undefined, 1), pos)
647
}, {
648
label: 'äbc',
649
kind: CompletionItemKind.Property,
650
insertText: 'äbc',
651
range: Range.fromPositions(pos.with(undefined, 1), pos)
652
}]
653
};
654
}
655
}));
656
657
model.setValue('');
658
return withOracle((model, editor) => {
659
660
return assertEvent(model.onDidSuggest, () => {
661
editor.setPosition({ lineNumber: 1, column: 1 });
662
editor.trigger('keyboard', Handler.Type, { text: 'a' });
663
}, event => {
664
assert.strictEqual(event.completionModel.items.length, 1);
665
assert.strictEqual(event.completionModel.items[0].completion.label, 'abc');
666
667
return assertEvent(model.onDidSuggest, () => {
668
editor.executeEdits('test', [EditOperation.replace(new Range(1, 1, 1, 2), 'ä')]);
669
670
}, event => {
671
// suggest model changed to äbc
672
assert.strictEqual(event.completionModel.items.length, 1);
673
assert.strictEqual(event.completionModel.items[0].completion.label, 'äbc');
674
675
});
676
});
677
});
678
});
679
680
test('Backspace should not always cancel code completion, #36491', function () {
681
disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));
682
683
return withOracle(async (model, editor) => {
684
await assertEvent(model.onDidSuggest, () => {
685
editor.setPosition({ lineNumber: 1, column: 4 });
686
editor.trigger('keyboard', Handler.Type, { text: 'd' });
687
688
}, event => {
689
assert.strictEqual(event.triggerOptions.auto, true);
690
assert.strictEqual(event.completionModel.items.length, 1);
691
const [first] = event.completionModel.items;
692
693
assert.strictEqual(first.provider, alwaysSomethingSupport);
694
});
695
696
await assertEvent(model.onDidSuggest, () => {
697
editor.runCommand(CoreEditingCommands.DeleteLeft, null);
698
699
}, event => {
700
assert.strictEqual(event.triggerOptions.auto, true);
701
assert.strictEqual(event.completionModel.items.length, 1);
702
const [first] = event.completionModel.items;
703
704
assert.strictEqual(first.provider, alwaysSomethingSupport);
705
});
706
});
707
});
708
709
test('Text changes for completion CodeAction are affected by the completion #39893', function () {
710
disposables.add(registry.register({ scheme: 'test' }, {
711
_debugDisplayName: 'test',
712
provideCompletionItems(doc, pos): CompletionList {
713
return {
714
incomplete: true,
715
suggestions: [{
716
label: 'bar',
717
kind: CompletionItemKind.Property,
718
insertText: 'bar',
719
range: Range.fromPositions(pos.delta(0, -2), pos),
720
additionalTextEdits: [{
721
text: ', bar',
722
range: { startLineNumber: 1, endLineNumber: 1, startColumn: 17, endColumn: 17 }
723
}]
724
}]
725
};
726
}
727
}));
728
729
model.setValue('ba; import { foo } from "./b"');
730
731
return withOracle(async (sugget, editor) => {
732
class TestCtrl extends SuggestController {
733
_insertSuggestion_publicForTest(item: ISelectedSuggestion, flags: number = 0) {
734
super._insertSuggestion(item, flags);
735
}
736
}
737
const ctrl = <TestCtrl>editor.registerAndInstantiateContribution(TestCtrl.ID, TestCtrl);
738
editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);
739
740
await assertEvent(sugget.onDidSuggest, () => {
741
editor.setPosition({ lineNumber: 1, column: 3 });
742
sugget.trigger({ auto: false });
743
}, event => {
744
745
assert.strictEqual(event.completionModel.items.length, 1);
746
const [first] = event.completionModel.items;
747
assert.strictEqual(first.completion.label, 'bar');
748
749
ctrl._insertSuggestion_publicForTest({ item: first, index: 0, model: event.completionModel });
750
});
751
752
assert.strictEqual(
753
model.getValue(),
754
'bar; import { foo, bar } from "./b"'
755
);
756
});
757
});
758
759
test('Completion unexpectedly triggers on second keypress of an edit group in a snippet #43523', function () {
760
761
disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));
762
763
return withOracle((model, editor) => {
764
return assertEvent(model.onDidSuggest, () => {
765
editor.setValue('d');
766
editor.setSelection(new Selection(1, 1, 1, 2));
767
editor.trigger('keyboard', Handler.Type, { text: 'e' });
768
769
}, event => {
770
assert.strictEqual(event.triggerOptions.auto, true);
771
assert.strictEqual(event.completionModel.items.length, 1);
772
const [first] = event.completionModel.items;
773
774
assert.strictEqual(first.provider, alwaysSomethingSupport);
775
});
776
});
777
});
778
779
780
test('Fails to render completion details #47988', function () {
781
782
let disposeA = 0;
783
let disposeB = 0;
784
785
disposables.add(registry.register({ scheme: 'test' }, {
786
_debugDisplayName: 'test',
787
provideCompletionItems(doc, pos) {
788
return {
789
incomplete: true,
790
suggestions: [{
791
kind: CompletionItemKind.Folder,
792
label: 'CompleteNot',
793
insertText: 'Incomplete',
794
sortText: 'a',
795
range: getDefaultSuggestRange(doc, pos)
796
}],
797
dispose() { disposeA += 1; }
798
};
799
}
800
}));
801
disposables.add(registry.register({ scheme: 'test' }, {
802
_debugDisplayName: 'test',
803
provideCompletionItems(doc, pos) {
804
return {
805
incomplete: false,
806
suggestions: [{
807
kind: CompletionItemKind.Folder,
808
label: 'Complete',
809
insertText: 'Complete',
810
sortText: 'z',
811
range: getDefaultSuggestRange(doc, pos)
812
}],
813
dispose() { disposeB += 1; }
814
};
815
},
816
resolveCompletionItem(item) {
817
return item;
818
},
819
}));
820
821
return withOracle(async (model, editor) => {
822
823
await assertEvent(model.onDidSuggest, () => {
824
editor.setValue('');
825
editor.setSelection(new Selection(1, 1, 1, 1));
826
editor.trigger('keyboard', Handler.Type, { text: 'c' });
827
828
}, event => {
829
assert.strictEqual(event.triggerOptions.auto, true);
830
assert.strictEqual(event.completionModel.items.length, 2);
831
assert.strictEqual(disposeA, 0);
832
assert.strictEqual(disposeB, 0);
833
});
834
835
await assertEvent(model.onDidSuggest, () => {
836
editor.trigger('keyboard', Handler.Type, { text: 'o' });
837
}, event => {
838
assert.strictEqual(event.triggerOptions.auto, true);
839
assert.strictEqual(event.completionModel.items.length, 2);
840
841
// clean up
842
model.clear();
843
assert.strictEqual(disposeA, 2); // provide got called two times!
844
assert.strictEqual(disposeB, 1);
845
});
846
847
});
848
});
849
850
851
test('Trigger (full) completions when (incomplete) completions are already active #99504', function () {
852
853
let countA = 0;
854
let countB = 0;
855
856
disposables.add(registry.register({ scheme: 'test' }, {
857
_debugDisplayName: 'test',
858
provideCompletionItems(doc, pos) {
859
countA += 1;
860
return {
861
incomplete: false, // doesn't matter if incomplete or not
862
suggestions: [{
863
kind: CompletionItemKind.Class,
864
label: 'Z aaa',
865
insertText: 'Z aaa',
866
range: new Range(1, 1, pos.lineNumber, pos.column)
867
}],
868
};
869
}
870
}));
871
disposables.add(registry.register({ scheme: 'test' }, {
872
_debugDisplayName: 'test',
873
provideCompletionItems(doc, pos) {
874
countB += 1;
875
if (!doc.getWordUntilPosition(pos).word.startsWith('a')) {
876
return;
877
}
878
return {
879
incomplete: false,
880
suggestions: [{
881
kind: CompletionItemKind.Folder,
882
label: 'aaa',
883
insertText: 'aaa',
884
range: getDefaultSuggestRange(doc, pos)
885
}],
886
};
887
},
888
}));
889
890
return withOracle(async (model, editor) => {
891
892
await assertEvent(model.onDidSuggest, () => {
893
editor.setValue('');
894
editor.setSelection(new Selection(1, 1, 1, 1));
895
editor.trigger('keyboard', Handler.Type, { text: 'Z' });
896
897
}, event => {
898
assert.strictEqual(event.triggerOptions.auto, true);
899
assert.strictEqual(event.completionModel.items.length, 1);
900
assert.strictEqual(event.completionModel.items[0].textLabel, 'Z aaa');
901
});
902
903
await assertEvent(model.onDidSuggest, () => {
904
// started another word: Z a|
905
// item should be: Z aaa, aaa
906
editor.trigger('keyboard', Handler.Type, { text: ' a' });
907
}, event => {
908
assert.strictEqual(event.triggerOptions.auto, true);
909
assert.strictEqual(event.completionModel.items.length, 2);
910
assert.strictEqual(event.completionModel.items[0].textLabel, 'Z aaa');
911
assert.strictEqual(event.completionModel.items[1].textLabel, 'aaa');
912
913
assert.strictEqual(countA, 1); // should we keep the suggestions from the "active" provider?, Yes! See: #106573
914
assert.strictEqual(countB, 2);
915
});
916
});
917
});
918
919
test('registerCompletionItemProvider with letters as trigger characters block other completion items to show up #127815', async function () {
920
921
disposables.add(registry.register({ scheme: 'test' }, {
922
_debugDisplayName: 'test',
923
provideCompletionItems(doc, pos) {
924
return {
925
suggestions: [{
926
kind: CompletionItemKind.Class,
927
label: 'AAAA',
928
insertText: 'WordTriggerA',
929
range: new Range(pos.lineNumber, pos.column, pos.lineNumber, pos.column)
930
}],
931
};
932
}
933
}));
934
disposables.add(registry.register({ scheme: 'test' }, {
935
_debugDisplayName: 'test',
936
triggerCharacters: ['a', '.'],
937
provideCompletionItems(doc, pos) {
938
return {
939
suggestions: [{
940
kind: CompletionItemKind.Class,
941
label: 'AAAA',
942
insertText: 'AutoTriggerA',
943
range: new Range(pos.lineNumber, pos.column, pos.lineNumber, pos.column)
944
}],
945
};
946
},
947
}));
948
949
return withOracle(async (model, editor) => {
950
951
await assertEvent(model.onDidSuggest, () => {
952
editor.setValue('');
953
editor.setSelection(new Selection(1, 1, 1, 1));
954
editor.trigger('keyboard', Handler.Type, { text: '.' });
955
956
}, event => {
957
assert.strictEqual(event.triggerOptions.auto, true);
958
assert.strictEqual(event.completionModel.items.length, 1);
959
});
960
961
962
editor.getModel().setValue('');
963
964
await assertEvent(model.onDidSuggest, () => {
965
editor.setValue('');
966
editor.setSelection(new Selection(1, 1, 1, 1));
967
editor.trigger('keyboard', Handler.Type, { text: 'a' });
968
969
}, event => {
970
assert.strictEqual(event.triggerOptions.auto, true);
971
assert.strictEqual(event.completionModel.items.length, 2);
972
});
973
});
974
});
975
976
test('Unexpected suggest scoring #167242', async function () {
977
disposables.add(registry.register('*', {
978
// word-based
979
_debugDisplayName: 'test',
980
provideCompletionItems(doc, pos) {
981
const word = doc.getWordUntilPosition(pos);
982
return {
983
suggestions: [{
984
kind: CompletionItemKind.Text,
985
label: 'pull',
986
insertText: 'pull',
987
range: new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn)
988
}],
989
};
990
}
991
}));
992
disposables.add(registry.register({ scheme: 'test' }, {
993
// JSON-based
994
_debugDisplayName: 'test',
995
provideCompletionItems(doc, pos) {
996
return {
997
suggestions: [{
998
kind: CompletionItemKind.Class,
999
label: 'git.pull',
1000
insertText: 'git.pull',
1001
range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)
1002
}],
1003
};
1004
},
1005
}));
1006
1007
return withOracle(async function (model, editor) {
1008
1009
await assertEvent(model.onDidSuggest, () => {
1010
editor.setValue('gi');
1011
editor.setSelection(new Selection(1, 3, 1, 3));
1012
editor.trigger('keyboard', Handler.Type, { text: 't' });
1013
1014
}, event => {
1015
assert.strictEqual(event.triggerOptions.auto, true);
1016
assert.strictEqual(event.completionModel.items.length, 1);
1017
assert.strictEqual(event.completionModel.items[0].textLabel, 'git.pull');
1018
});
1019
1020
editor.trigger('keyboard', Handler.Type, { text: '.' });
1021
1022
await assertEvent(model.onDidSuggest, () => {
1023
editor.trigger('keyboard', Handler.Type, { text: 'p' });
1024
1025
}, event => {
1026
assert.strictEqual(event.triggerOptions.auto, true);
1027
assert.strictEqual(event.completionModel.items.length, 1);
1028
assert.strictEqual(event.completionModel.items[0].textLabel, 'git.pull');
1029
});
1030
});
1031
});
1032
1033
test('Completion list closes unexpectedly when typing a digit after a word separator #169390', function () {
1034
1035
const requestCounts = [0, 0];
1036
1037
disposables.add(registry.register({ scheme: 'test' }, {
1038
_debugDisplayName: 'test',
1039
1040
provideCompletionItems(doc, pos) {
1041
requestCounts[0] += 1;
1042
return {
1043
suggestions: [{
1044
kind: CompletionItemKind.Text,
1045
label: 'foo-20',
1046
insertText: 'foo-20',
1047
range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)
1048
}, {
1049
kind: CompletionItemKind.Text,
1050
label: 'foo-hello',
1051
insertText: 'foo-hello',
1052
range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)
1053
}],
1054
};
1055
}
1056
}));
1057
disposables.add(registry.register({ scheme: 'test' }, {
1058
_debugDisplayName: 'test',
1059
triggerCharacters: ['2'],
1060
provideCompletionItems(doc, pos, ctx) {
1061
requestCounts[1] += 1;
1062
if (ctx.triggerKind !== CompletionTriggerKind.TriggerCharacter) {
1063
return;
1064
}
1065
return {
1066
suggestions: [{
1067
kind: CompletionItemKind.Class,
1068
label: 'foo-210',
1069
insertText: 'foo-210',
1070
range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)
1071
}],
1072
};
1073
},
1074
}));
1075
1076
return withOracle(async function (model, editor) {
1077
1078
await assertEvent(model.onDidSuggest, () => {
1079
editor.setValue('foo');
1080
editor.setSelection(new Selection(1, 4, 1, 4));
1081
model.trigger({ auto: false });
1082
1083
}, event => {
1084
assert.strictEqual(event.triggerOptions.auto, false);
1085
assert.strictEqual(event.completionModel.items.length, 2);
1086
assert.strictEqual(event.completionModel.items[0].textLabel, 'foo-20');
1087
assert.strictEqual(event.completionModel.items[1].textLabel, 'foo-hello');
1088
});
1089
1090
editor.trigger('keyboard', Handler.Type, { text: '-' });
1091
1092
1093
await assertEvent(model.onDidSuggest, () => {
1094
editor.trigger('keyboard', Handler.Type, { text: '2' });
1095
1096
}, event => {
1097
assert.strictEqual(event.triggerOptions.auto, true);
1098
assert.strictEqual(event.completionModel.items.length, 2);
1099
assert.strictEqual(event.completionModel.items[0].textLabel, 'foo-20');
1100
assert.strictEqual(event.completionModel.items[1].textLabel, 'foo-210');
1101
assert.deepStrictEqual(requestCounts, [1, 2]);
1102
});
1103
});
1104
});
1105
1106
test('Set refilter-flag, keep triggerKind', function () {
1107
1108
disposables.add(registry.register({ scheme: 'test' }, {
1109
_debugDisplayName: 'test',
1110
triggerCharacters: ['.'],
1111
provideCompletionItems(doc, pos, ctx) {
1112
return {
1113
suggestions: [{
1114
label: doc.getWordUntilPosition(pos).word || 'hello',
1115
kind: CompletionItemKind.Property,
1116
insertText: 'foofoo',
1117
range: getDefaultSuggestRange(doc, pos)
1118
}]
1119
};
1120
},
1121
}));
1122
1123
return withOracle(async function (model, editor) {
1124
1125
await assertEvent(model.onDidSuggest, () => {
1126
editor.setValue('foo');
1127
editor.setSelection(new Selection(1, 4, 1, 4));
1128
editor.trigger('keyboard', Handler.Type, { text: 'o' });
1129
1130
1131
}, event => {
1132
assert.strictEqual(event.triggerOptions.auto, true);
1133
assert.strictEqual(event.triggerOptions.triggerCharacter, undefined);
1134
assert.strictEqual(event.triggerOptions.triggerKind, undefined);
1135
assert.strictEqual(event.completionModel.items.length, 1);
1136
});
1137
1138
await assertEvent(model.onDidSuggest, () => {
1139
editor.trigger('keyboard', Handler.Type, { text: '.' });
1140
1141
}, event => {
1142
assert.strictEqual(event.triggerOptions.auto, true);
1143
assert.strictEqual(event.triggerOptions.refilter, undefined);
1144
assert.strictEqual(event.triggerOptions.triggerCharacter, '.');
1145
assert.strictEqual(event.triggerOptions.triggerKind, CompletionTriggerKind.TriggerCharacter);
1146
assert.strictEqual(event.completionModel.items.length, 1);
1147
});
1148
1149
await assertEvent(model.onDidSuggest, () => {
1150
editor.trigger('keyboard', Handler.Type, { text: 'h' });
1151
1152
}, event => {
1153
assert.strictEqual(event.triggerOptions.auto, true);
1154
assert.strictEqual(event.triggerOptions.refilter, true);
1155
assert.strictEqual(event.triggerOptions.triggerCharacter, '.');
1156
assert.strictEqual(event.triggerOptions.triggerKind, CompletionTriggerKind.TriggerCharacter);
1157
assert.strictEqual(event.completionModel.items.length, 1);
1158
});
1159
});
1160
});
1161
1162
test('Snippets gone from IntelliSense #173244', function () {
1163
1164
const snippetProvider: CompletionItemProvider = {
1165
_debugDisplayName: 'test',
1166
provideCompletionItems(doc, pos, ctx) {
1167
return {
1168
suggestions: [{
1169
label: 'log',
1170
kind: CompletionItemKind.Snippet,
1171
insertText: 'log',
1172
range: getDefaultSuggestRange(doc, pos)
1173
}]
1174
};
1175
}
1176
};
1177
const old = setSnippetSuggestSupport(snippetProvider);
1178
1179
disposables.add(toDisposable(() => {
1180
if (getSnippetSuggestSupport() === snippetProvider) {
1181
setSnippetSuggestSupport(old);
1182
}
1183
}));
1184
1185
disposables.add(registry.register({ scheme: 'test' }, {
1186
_debugDisplayName: 'test',
1187
triggerCharacters: ['.'],
1188
provideCompletionItems(doc, pos, ctx) {
1189
return {
1190
suggestions: [{
1191
label: 'locals',
1192
kind: CompletionItemKind.Property,
1193
insertText: 'locals',
1194
range: getDefaultSuggestRange(doc, pos)
1195
}],
1196
incomplete: true
1197
};
1198
},
1199
}));
1200
1201
return withOracle(async function (model, editor) {
1202
1203
await assertEvent(model.onDidSuggest, () => {
1204
editor.setValue('');
1205
editor.setSelection(new Selection(1, 1, 1, 1));
1206
editor.trigger('keyboard', Handler.Type, { text: 'l' });
1207
1208
1209
}, event => {
1210
assert.strictEqual(event.triggerOptions.auto, true);
1211
assert.strictEqual(event.triggerOptions.triggerCharacter, undefined);
1212
assert.strictEqual(event.triggerOptions.triggerKind, undefined);
1213
assert.strictEqual(event.completionModel.items.length, 2);
1214
assert.strictEqual(event.completionModel.items[0].textLabel, 'locals');
1215
assert.strictEqual(event.completionModel.items[1].textLabel, 'log');
1216
});
1217
1218
await assertEvent(model.onDidSuggest, () => {
1219
editor.trigger('keyboard', Handler.Type, { text: 'o' });
1220
1221
}, event => {
1222
assert.strictEqual(event.triggerOptions.triggerKind, CompletionTriggerKind.TriggerForIncompleteCompletions);
1223
assert.strictEqual(event.triggerOptions.auto, true);
1224
assert.strictEqual(event.completionModel.items.length, 2);
1225
assert.strictEqual(event.completionModel.items[0].textLabel, 'locals');
1226
assert.strictEqual(event.completionModel.items[1].textLabel, 'log');
1227
});
1228
1229
});
1230
});
1231
});
1232
1233