Path: blob/main/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts
5263 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import { Event } from '../../../../../base/common/event.js';6import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';7import { URI } from '../../../../../base/common/uri.js';8import { mock } from '../../../../../base/test/common/mock.js';9import { CoreEditingCommands } from '../../../../browser/coreCommands.js';10import { EditOperation } from '../../../../common/core/editOperation.js';11import { Position } from '../../../../common/core/position.js';12import { Range } from '../../../../common/core/range.js';13import { Selection } from '../../../../common/core/selection.js';14import { Handler } from '../../../../common/editorCommon.js';15import { ITextModel } from '../../../../common/model.js';16import { TextModel } from '../../../../common/model/textModel.js';17import { CompletionItemKind, CompletionItemProvider, CompletionList, CompletionTriggerKind, EncodedTokenizationResult, InlineCompletionsProvider, IState, TokenizationRegistry } from '../../../../common/languages.js';18import { MetadataConsts } from '../../../../common/encodedTokenAttributes.js';19import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';20import { NullState } from '../../../../common/languages/nullTokenize.js';21import { ILanguageService } from '../../../../common/languages/language.js';22import { SnippetController2 } from '../../../snippet/browser/snippetController2.js';23import { SuggestController } from '../../browser/suggestController.js';24import { ISuggestMemoryService } from '../../browser/suggestMemory.js';25import { LineContext, SuggestModel } from '../../browser/suggestModel.js';26import { ISelectedSuggestion } from '../../browser/suggestWidget.js';27import { createTestCodeEditor, ITestCodeEditor } from '../../../../test/browser/testCodeEditor.js';28import { createModelServices, createTextModel, instantiateTextModel } from '../../../../test/common/testTextModel.js';29import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';30import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';31import { MockKeybindingService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';32import { ILabelService } from '../../../../../platform/label/common/label.js';33import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';34import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';35import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';36import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';37import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';38import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';39import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';40import { getSnippetSuggestSupport, setSnippetSuggestSupport } from '../../browser/suggest.js';41import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';42import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';43import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';444546function createMockEditor(model: TextModel, languageFeaturesService: ILanguageFeaturesService): ITestCodeEditor {4748const storeService = new InMemoryStorageService();49const editor = createTestCodeEditor(model, {50serviceCollection: new ServiceCollection(51[ILanguageFeaturesService, languageFeaturesService],52[ITelemetryService, NullTelemetryService],53[IStorageService, storeService],54[IKeybindingService, new MockKeybindingService()],55[ISuggestMemoryService, new class implements ISuggestMemoryService {56declare readonly _serviceBrand: undefined;57memorize(): void {58}59select(): number {60return -1;61}62}],63[ILabelService, new class extends mock<ILabelService>() { }],64[IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { }],65[IEnvironmentService, new class extends mock<IEnvironmentService>() {66override isBuilt: boolean = true;67override isExtensionDevelopment: boolean = false;68}],69),70});71const ctrl = editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);72editor.hasWidgetFocus = () => true;7374editor.registerDisposable(ctrl);75editor.registerDisposable(storeService);76return editor;77}7879suite('SuggestModel - Context', function () {80const OUTER_LANGUAGE_ID = 'outerMode';81const INNER_LANGUAGE_ID = 'innerMode';8283class OuterMode extends Disposable {84public readonly languageId = OUTER_LANGUAGE_ID;85constructor(86@ILanguageService languageService: ILanguageService,87@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService,88) {89super();90this._register(languageService.registerLanguage({ id: this.languageId }));91this._register(languageConfigurationService.register(this.languageId, {}));9293this._register(TokenizationRegistry.register(this.languageId, {94getInitialState: (): IState => NullState,95tokenize: undefined!,96tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => {97const tokensArr: number[] = [];98let prevLanguageId: string | undefined = undefined;99for (let i = 0; i < line.length; i++) {100const languageId = (line.charAt(i) === 'x' ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID);101const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId);102if (prevLanguageId !== languageId) {103tokensArr.push(i);104tokensArr.push((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET));105}106prevLanguageId = languageId;107}108109const tokens = new Uint32Array(tokensArr.length);110for (let i = 0; i < tokens.length; i++) {111tokens[i] = tokensArr[i];112}113return new EncodedTokenizationResult(tokens, [], state);114}115}));116}117}118119class InnerMode extends Disposable {120public readonly languageId = INNER_LANGUAGE_ID;121constructor(122@ILanguageService languageService: ILanguageService,123@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService124) {125super();126this._register(languageService.registerLanguage({ id: this.languageId }));127this._register(languageConfigurationService.register(this.languageId, {}));128}129}130131const assertAutoTrigger = (model: TextModel, offset: number, expected: boolean, message?: string): void => {132const pos = model.getPositionAt(offset);133const editor = createMockEditor(model, new LanguageFeaturesService());134editor.setPosition(pos);135assert.strictEqual(LineContext.shouldAutoTrigger(editor), expected, message);136editor.dispose();137};138139let disposables: DisposableStore;140141setup(() => {142disposables = new DisposableStore();143});144145teardown(function () {146disposables.dispose();147});148149ensureNoDisposablesAreLeakedInTestSuite();150151test('Context - shouldAutoTrigger', function () {152const model = createTextModel('Das Pferd frisst keinen Gurkensalat - Philipp Reis 1861.\nWer hat\'s erfunden?');153disposables.add(model);154155assertAutoTrigger(model, 3, true, 'end of word, Das|');156assertAutoTrigger(model, 4, false, 'no word Das |');157assertAutoTrigger(model, 1, true, 'typing a single character before a word: D|as');158assertAutoTrigger(model, 55, false, 'number, 1861|');159model.dispose();160});161162test('shouldAutoTrigger at embedded language boundaries', () => {163const disposables = new DisposableStore();164const instantiationService = createModelServices(disposables);165const outerMode = disposables.add(instantiationService.createInstance(OuterMode));166disposables.add(instantiationService.createInstance(InnerMode));167168const model = disposables.add(instantiateTextModel(instantiationService, 'a<xx>a<x>', outerMode.languageId));169170assertAutoTrigger(model, 1, true, 'a|<x — should trigger at end of word');171assertAutoTrigger(model, 2, false, 'a<|x — should NOT trigger at start of word');172assertAutoTrigger(model, 3, true, 'a<x|x — should trigger after typing a single character before a word');173assertAutoTrigger(model, 4, true, 'a<xx|> — should trigger at boundary between languages');174assertAutoTrigger(model, 5, false, 'a<xx>|a — should NOT trigger at start of word');175assertAutoTrigger(model, 6, true, 'a<xx>a|< — should trigger at end of word');176assertAutoTrigger(model, 8, true, 'a<xx>a<x|> — should trigger at end of word at boundary');177178disposables.dispose();179});180});181182suite('SuggestModel - TriggerAndCancelOracle', function () {183184185function getDefaultSuggestRange(model: ITextModel, position: Position) {186const wordUntil = model.getWordUntilPosition(position);187return new Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);188}189190const alwaysEmptySupport: CompletionItemProvider = {191_debugDisplayName: 'test',192provideCompletionItems(doc, pos): CompletionList {193return {194incomplete: false,195suggestions: []196};197}198};199200const alwaysSomethingSupport: CompletionItemProvider = {201_debugDisplayName: 'test',202provideCompletionItems(doc, pos): CompletionList {203return {204incomplete: false,205suggestions: [{206label: doc.getWordUntilPosition(pos).word,207kind: CompletionItemKind.Property,208insertText: 'foofoo',209range: getDefaultSuggestRange(doc, pos)210}]211};212}213};214215let disposables: DisposableStore;216let model: TextModel;217const languageFeaturesService = new LanguageFeaturesService();218const registry = languageFeaturesService.completionProvider;219220setup(function () {221disposables = new DisposableStore();222model = createTextModel('abc def', undefined, undefined, URI.parse('test:somefile.ttt'));223disposables.add(model);224});225226teardown(() => {227disposables.dispose();228});229230ensureNoDisposablesAreLeakedInTestSuite();231232function withOracle(callback: (model: SuggestModel, editor: ITestCodeEditor) => any): Promise<any> {233234return new Promise((resolve, reject) => {235const editor = createMockEditor(model, languageFeaturesService);236const oracle = editor.invokeWithinContext(accessor => accessor.get(IInstantiationService).createInstance(SuggestModel, editor));237disposables.add(oracle);238disposables.add(editor);239240try {241resolve(callback(oracle, editor));242} catch (err) {243reject(err);244}245});246}247248function assertEvent<E>(event: Event<E>, action: () => any, assert: (e: E) => any) {249return new Promise((resolve, reject) => {250const sub = event(e => {251sub.dispose();252try {253resolve(assert(e));254} catch (err) {255reject(err);256}257});258try {259action();260} catch (err) {261sub.dispose();262reject(err);263}264});265}266267test('events - cancel/trigger', function () {268return withOracle(model => {269270return Promise.all([271272assertEvent(model.onDidTrigger, function () {273model.trigger({ auto: true });274}, function (event) {275assert.strictEqual(event.auto, true);276277return assertEvent(model.onDidCancel, function () {278model.cancel();279}, function (event) {280assert.strictEqual(event.retrigger, false);281});282}),283284assertEvent(model.onDidTrigger, function () {285model.trigger({ auto: true });286}, function (event) {287assert.strictEqual(event.auto, true);288}),289290assertEvent(model.onDidTrigger, function () {291model.trigger({ auto: false });292}, function (event) {293assert.strictEqual(event.auto, false);294})295]);296});297});298299300test('events - suggest/empty', function () {301302disposables.add(registry.register({ scheme: 'test' }, alwaysEmptySupport));303304return withOracle(model => {305return Promise.all([306assertEvent(model.onDidCancel, function () {307model.trigger({ auto: true });308}, function (event) {309assert.strictEqual(event.retrigger, false);310}),311assertEvent(model.onDidSuggest, function () {312model.trigger({ auto: false });313}, function (event) {314assert.strictEqual(event.triggerOptions.auto, false);315assert.strictEqual(event.isFrozen, false);316assert.strictEqual(event.completionModel.items.length, 0);317})318]);319});320});321322test('trigger - on type', function () {323324disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));325326return withOracle((model, editor) => {327return assertEvent(model.onDidSuggest, () => {328editor.setPosition({ lineNumber: 1, column: 4 });329editor.trigger('keyboard', Handler.Type, { text: 'd' });330331}, event => {332assert.strictEqual(event.triggerOptions.auto, true);333assert.strictEqual(event.completionModel.items.length, 1);334const [first] = event.completionModel.items;335336assert.strictEqual(first.provider, alwaysSomethingSupport);337});338});339});340341test('#17400: Keep filtering suggestModel.ts after space', function () {342343disposables.add(registry.register({ scheme: 'test' }, {344_debugDisplayName: 'test',345provideCompletionItems(doc, pos): CompletionList {346return {347incomplete: false,348suggestions: [{349label: 'My Table',350kind: CompletionItemKind.Property,351insertText: 'My Table',352range: getDefaultSuggestRange(doc, pos)353}]354};355}356}));357358model.setValue('');359360return withOracle((model, editor) => {361362return assertEvent(model.onDidSuggest, () => {363// make sure completionModel starts here!364model.trigger({ auto: true });365}, event => {366367return assertEvent(model.onDidSuggest, () => {368editor.setPosition({ lineNumber: 1, column: 1 });369editor.trigger('keyboard', Handler.Type, { text: 'My' });370371}, event => {372assert.strictEqual(event.triggerOptions.auto, true);373assert.strictEqual(event.completionModel.items.length, 1);374const [first] = event.completionModel.items;375assert.strictEqual(first.completion.label, 'My Table');376377return assertEvent(model.onDidSuggest, () => {378editor.setPosition({ lineNumber: 1, column: 3 });379editor.trigger('keyboard', Handler.Type, { text: ' ' });380381}, event => {382assert.strictEqual(event.triggerOptions.auto, true);383assert.strictEqual(event.completionModel.items.length, 1);384const [first] = event.completionModel.items;385assert.strictEqual(first.completion.label, 'My Table');386});387});388});389});390});391392test('#21484: Trigger character always force a new completion session', function () {393394disposables.add(registry.register({ scheme: 'test' }, {395_debugDisplayName: 'test',396provideCompletionItems(doc, pos): CompletionList {397return {398incomplete: false,399suggestions: [{400label: 'foo.bar',401kind: CompletionItemKind.Property,402insertText: 'foo.bar',403range: Range.fromPositions(pos.with(undefined, 1), pos)404}]405};406}407}));408409disposables.add(registry.register({ scheme: 'test' }, {410_debugDisplayName: 'test',411triggerCharacters: ['.'],412provideCompletionItems(doc, pos): CompletionList {413return {414incomplete: false,415suggestions: [{416label: 'boom',417kind: CompletionItemKind.Property,418insertText: 'boom',419range: Range.fromPositions(420pos.delta(0, doc.getLineContent(pos.lineNumber)[pos.column - 2] === '.' ? 0 : -1),421pos422)423}]424};425}426}));427428model.setValue('');429430return withOracle(async (model, editor) => {431432await assertEvent(model.onDidSuggest, () => {433editor.setPosition({ lineNumber: 1, column: 1 });434editor.trigger('keyboard', Handler.Type, { text: 'foo' });435436}, event => {437assert.strictEqual(event.triggerOptions.auto, true);438assert.strictEqual(event.completionModel.items.length, 1);439const [first] = event.completionModel.items;440assert.strictEqual(first.completion.label, 'foo.bar');441442});443444await assertEvent(model.onDidSuggest, () => {445editor.trigger('keyboard', Handler.Type, { text: '.' });446447}, event => {448// SYNC449assert.strictEqual(event.triggerOptions.auto, true);450assert.strictEqual(event.completionModel.items.length, 1);451const [first] = event.completionModel.items;452assert.strictEqual(first.completion.label, 'foo.bar');453});454455await assertEvent(model.onDidSuggest, () => {456// nothing -> triggered by the trigger character typing (see above)457458}, event => {459// ASYNC460assert.strictEqual(event.triggerOptions.auto, true);461assert.strictEqual(event.completionModel.items.length, 2);462const [first, second] = event.completionModel.items;463assert.strictEqual(first.completion.label, 'foo.bar');464assert.strictEqual(second.completion.label, 'boom');465});466});467});468469test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [1/2]', function () {470471disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));472473return withOracle((model, editor) => {474475editor.getModel()!.setValue('fo');476editor.setPosition({ lineNumber: 1, column: 3 });477478return assertEvent(model.onDidSuggest, () => {479model.trigger({ auto: false });480}, event => {481assert.strictEqual(event.triggerOptions.auto, false);482assert.strictEqual(event.isFrozen, false);483assert.strictEqual(event.completionModel.items.length, 1);484485return assertEvent(model.onDidCancel, () => {486editor.trigger('keyboard', Handler.Type, { text: '+' });487}, event => {488assert.strictEqual(event.retrigger, false);489});490});491});492});493494test('Intellisense Completion doesn\'t respect space after equal sign (.html file), #29353 [2/2]', function () {495496disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));497498return withOracle((model, editor) => {499500editor.getModel()!.setValue('fo');501editor.setPosition({ lineNumber: 1, column: 3 });502503return assertEvent(model.onDidSuggest, () => {504model.trigger({ auto: false });505}, event => {506assert.strictEqual(event.triggerOptions.auto, false);507assert.strictEqual(event.isFrozen, false);508assert.strictEqual(event.completionModel.items.length, 1);509510return assertEvent(model.onDidCancel, () => {511editor.trigger('keyboard', Handler.Type, { text: ' ' });512}, event => {513assert.strictEqual(event.retrigger, false);514});515});516});517});518519test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (1/2)', function () {520521disposables.add(registry.register({ scheme: 'test' }, {522_debugDisplayName: 'test',523provideCompletionItems(doc, pos): CompletionList {524return {525incomplete: true,526suggestions: [{527label: 'foo',528kind: CompletionItemKind.Property,529insertText: 'foo',530range: Range.fromPositions(pos.with(undefined, 1), pos)531}]532};533}534}));535536return withOracle((model, editor) => {537538editor.getModel()!.setValue('foo');539editor.setPosition({ lineNumber: 1, column: 4 });540541return assertEvent(model.onDidSuggest, () => {542model.trigger({ auto: false });543}, event => {544assert.strictEqual(event.triggerOptions.auto, false);545assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);546assert.strictEqual(event.completionModel.items.length, 1);547548return assertEvent(model.onDidCancel, () => {549editor.trigger('keyboard', Handler.Type, { text: ';' });550}, event => {551assert.strictEqual(event.retrigger, false);552});553});554});555});556557test('Incomplete suggestion results cause re-triggering when typing w/o further context, #28400 (2/2)', function () {558559disposables.add(registry.register({ scheme: 'test' }, {560_debugDisplayName: 'test',561provideCompletionItems(doc, pos): CompletionList {562return {563incomplete: true,564suggestions: [{565label: 'foo;',566kind: CompletionItemKind.Property,567insertText: 'foo',568range: Range.fromPositions(pos.with(undefined, 1), pos)569}]570};571}572}));573574return withOracle((model, editor) => {575576editor.getModel()!.setValue('foo');577editor.setPosition({ lineNumber: 1, column: 4 });578579return assertEvent(model.onDidSuggest, () => {580model.trigger({ auto: false });581}, event => {582assert.strictEqual(event.triggerOptions.auto, false);583assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);584assert.strictEqual(event.completionModel.items.length, 1);585586return assertEvent(model.onDidSuggest, () => {587// while we cancel incrementally enriching the set of588// completions we still filter against those that we have589// until now590editor.trigger('keyboard', Handler.Type, { text: ';' });591}, event => {592assert.strictEqual(event.triggerOptions.auto, false);593assert.strictEqual(event.completionModel.getIncompleteProvider().size, 1);594assert.strictEqual(event.completionModel.items.length, 1);595596});597});598});599});600601test('Trigger character is provided in suggest context', function () {602let triggerCharacter = '';603disposables.add(registry.register({ scheme: 'test' }, {604_debugDisplayName: 'test',605triggerCharacters: ['.'],606provideCompletionItems(doc, pos, context): CompletionList {607assert.strictEqual(context.triggerKind, CompletionTriggerKind.TriggerCharacter);608triggerCharacter = context.triggerCharacter!;609return {610incomplete: false,611suggestions: [612{613label: 'foo.bar',614kind: CompletionItemKind.Property,615insertText: 'foo.bar',616range: Range.fromPositions(pos.with(undefined, 1), pos)617}618]619};620}621}));622623model.setValue('');624625return withOracle((model, editor) => {626627return assertEvent(model.onDidSuggest, () => {628editor.setPosition({ lineNumber: 1, column: 1 });629editor.trigger('keyboard', Handler.Type, { text: 'foo.' });630}, event => {631assert.strictEqual(triggerCharacter, '.');632});633});634});635636test('Mac press and hold accent character insertion does not update suggestions, #35269', function () {637disposables.add(registry.register({ scheme: 'test' }, {638_debugDisplayName: 'test',639provideCompletionItems(doc, pos): CompletionList {640return {641incomplete: true,642suggestions: [{643label: 'abc',644kind: CompletionItemKind.Property,645insertText: 'abc',646range: Range.fromPositions(pos.with(undefined, 1), pos)647}, {648label: 'äbc',649kind: CompletionItemKind.Property,650insertText: 'äbc',651range: Range.fromPositions(pos.with(undefined, 1), pos)652}]653};654}655}));656657model.setValue('');658return withOracle((model, editor) => {659660return assertEvent(model.onDidSuggest, () => {661editor.setPosition({ lineNumber: 1, column: 1 });662editor.trigger('keyboard', Handler.Type, { text: 'a' });663}, event => {664assert.strictEqual(event.completionModel.items.length, 1);665assert.strictEqual(event.completionModel.items[0].completion.label, 'abc');666667return assertEvent(model.onDidSuggest, () => {668editor.executeEdits('test', [EditOperation.replace(new Range(1, 1, 1, 2), 'ä')]);669670}, event => {671// suggest model changed to äbc672assert.strictEqual(event.completionModel.items.length, 1);673assert.strictEqual(event.completionModel.items[0].completion.label, 'äbc');674675});676});677});678});679680test('Backspace should not always cancel code completion, #36491', function () {681disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));682683return withOracle(async (model, editor) => {684await assertEvent(model.onDidSuggest, () => {685editor.setPosition({ lineNumber: 1, column: 4 });686editor.trigger('keyboard', Handler.Type, { text: 'd' });687688}, event => {689assert.strictEqual(event.triggerOptions.auto, true);690assert.strictEqual(event.completionModel.items.length, 1);691const [first] = event.completionModel.items;692693assert.strictEqual(first.provider, alwaysSomethingSupport);694});695696await assertEvent(model.onDidSuggest, () => {697editor.runCommand(CoreEditingCommands.DeleteLeft, null);698699}, event => {700assert.strictEqual(event.triggerOptions.auto, true);701assert.strictEqual(event.completionModel.items.length, 1);702const [first] = event.completionModel.items;703704assert.strictEqual(first.provider, alwaysSomethingSupport);705});706});707});708709test('Text changes for completion CodeAction are affected by the completion #39893', function () {710disposables.add(registry.register({ scheme: 'test' }, {711_debugDisplayName: 'test',712provideCompletionItems(doc, pos): CompletionList {713return {714incomplete: true,715suggestions: [{716label: 'bar',717kind: CompletionItemKind.Property,718insertText: 'bar',719range: Range.fromPositions(pos.delta(0, -2), pos),720additionalTextEdits: [{721text: ', bar',722range: { startLineNumber: 1, endLineNumber: 1, startColumn: 17, endColumn: 17 }723}]724}]725};726}727}));728729model.setValue('ba; import { foo } from "./b"');730731return withOracle(async (sugget, editor) => {732class TestCtrl extends SuggestController {733_insertSuggestion_publicForTest(item: ISelectedSuggestion, flags: number = 0) {734super._insertSuggestion(item, flags);735}736}737const ctrl = <TestCtrl>editor.registerAndInstantiateContribution(TestCtrl.ID, TestCtrl);738editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);739740await assertEvent(sugget.onDidSuggest, () => {741editor.setPosition({ lineNumber: 1, column: 3 });742sugget.trigger({ auto: false });743}, event => {744745assert.strictEqual(event.completionModel.items.length, 1);746const [first] = event.completionModel.items;747assert.strictEqual(first.completion.label, 'bar');748749ctrl._insertSuggestion_publicForTest({ item: first, index: 0, model: event.completionModel });750});751752assert.strictEqual(753model.getValue(),754'bar; import { foo, bar } from "./b"'755);756});757});758759test('Completion unexpectedly triggers on second keypress of an edit group in a snippet #43523', function () {760761disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));762763return withOracle((model, editor) => {764return assertEvent(model.onDidSuggest, () => {765editor.setValue('d');766editor.setSelection(new Selection(1, 1, 1, 2));767editor.trigger('keyboard', Handler.Type, { text: 'e' });768769}, event => {770assert.strictEqual(event.triggerOptions.auto, true);771assert.strictEqual(event.completionModel.items.length, 1);772const [first] = event.completionModel.items;773774assert.strictEqual(first.provider, alwaysSomethingSupport);775});776});777});778779780test('Fails to render completion details #47988', function () {781782let disposeA = 0;783let disposeB = 0;784785disposables.add(registry.register({ scheme: 'test' }, {786_debugDisplayName: 'test',787provideCompletionItems(doc, pos) {788return {789incomplete: true,790suggestions: [{791kind: CompletionItemKind.Folder,792label: 'CompleteNot',793insertText: 'Incomplete',794sortText: 'a',795range: getDefaultSuggestRange(doc, pos)796}],797dispose() { disposeA += 1; }798};799}800}));801disposables.add(registry.register({ scheme: 'test' }, {802_debugDisplayName: 'test',803provideCompletionItems(doc, pos) {804return {805incomplete: false,806suggestions: [{807kind: CompletionItemKind.Folder,808label: 'Complete',809insertText: 'Complete',810sortText: 'z',811range: getDefaultSuggestRange(doc, pos)812}],813dispose() { disposeB += 1; }814};815},816resolveCompletionItem(item) {817return item;818},819}));820821return withOracle(async (model, editor) => {822823await assertEvent(model.onDidSuggest, () => {824editor.setValue('');825editor.setSelection(new Selection(1, 1, 1, 1));826editor.trigger('keyboard', Handler.Type, { text: 'c' });827828}, event => {829assert.strictEqual(event.triggerOptions.auto, true);830assert.strictEqual(event.completionModel.items.length, 2);831assert.strictEqual(disposeA, 0);832assert.strictEqual(disposeB, 0);833});834835await assertEvent(model.onDidSuggest, () => {836editor.trigger('keyboard', Handler.Type, { text: 'o' });837}, event => {838assert.strictEqual(event.triggerOptions.auto, true);839assert.strictEqual(event.completionModel.items.length, 2);840841// clean up842model.clear();843assert.strictEqual(disposeA, 2); // provide got called two times!844assert.strictEqual(disposeB, 1);845});846847});848});849850851test('Trigger (full) completions when (incomplete) completions are already active #99504', function () {852853let countA = 0;854let countB = 0;855856disposables.add(registry.register({ scheme: 'test' }, {857_debugDisplayName: 'test',858provideCompletionItems(doc, pos) {859countA += 1;860return {861incomplete: false, // doesn't matter if incomplete or not862suggestions: [{863kind: CompletionItemKind.Class,864label: 'Z aaa',865insertText: 'Z aaa',866range: new Range(1, 1, pos.lineNumber, pos.column)867}],868};869}870}));871disposables.add(registry.register({ scheme: 'test' }, {872_debugDisplayName: 'test',873provideCompletionItems(doc, pos) {874countB += 1;875if (!doc.getWordUntilPosition(pos).word.startsWith('a')) {876return;877}878return {879incomplete: false,880suggestions: [{881kind: CompletionItemKind.Folder,882label: 'aaa',883insertText: 'aaa',884range: getDefaultSuggestRange(doc, pos)885}],886};887},888}));889890return withOracle(async (model, editor) => {891892await assertEvent(model.onDidSuggest, () => {893editor.setValue('');894editor.setSelection(new Selection(1, 1, 1, 1));895editor.trigger('keyboard', Handler.Type, { text: 'Z' });896897}, event => {898assert.strictEqual(event.triggerOptions.auto, true);899assert.strictEqual(event.completionModel.items.length, 1);900assert.strictEqual(event.completionModel.items[0].textLabel, 'Z aaa');901});902903await assertEvent(model.onDidSuggest, () => {904// started another word: Z a|905// item should be: Z aaa, aaa906editor.trigger('keyboard', Handler.Type, { text: ' a' });907}, event => {908assert.strictEqual(event.triggerOptions.auto, true);909assert.strictEqual(event.completionModel.items.length, 2);910assert.strictEqual(event.completionModel.items[0].textLabel, 'Z aaa');911assert.strictEqual(event.completionModel.items[1].textLabel, 'aaa');912913assert.strictEqual(countA, 1); // should we keep the suggestions from the "active" provider?, Yes! See: #106573914assert.strictEqual(countB, 2);915});916});917});918919test('registerCompletionItemProvider with letters as trigger characters block other completion items to show up #127815', async function () {920921disposables.add(registry.register({ scheme: 'test' }, {922_debugDisplayName: 'test',923provideCompletionItems(doc, pos) {924return {925suggestions: [{926kind: CompletionItemKind.Class,927label: 'AAAA',928insertText: 'WordTriggerA',929range: new Range(pos.lineNumber, pos.column, pos.lineNumber, pos.column)930}],931};932}933}));934disposables.add(registry.register({ scheme: 'test' }, {935_debugDisplayName: 'test',936triggerCharacters: ['a', '.'],937provideCompletionItems(doc, pos) {938return {939suggestions: [{940kind: CompletionItemKind.Class,941label: 'AAAA',942insertText: 'AutoTriggerA',943range: new Range(pos.lineNumber, pos.column, pos.lineNumber, pos.column)944}],945};946},947}));948949return withOracle(async (model, editor) => {950951await assertEvent(model.onDidSuggest, () => {952editor.setValue('');953editor.setSelection(new Selection(1, 1, 1, 1));954editor.trigger('keyboard', Handler.Type, { text: '.' });955956}, event => {957assert.strictEqual(event.triggerOptions.auto, true);958assert.strictEqual(event.completionModel.items.length, 1);959});960961962editor.getModel().setValue('');963964await assertEvent(model.onDidSuggest, () => {965editor.setValue('');966editor.setSelection(new Selection(1, 1, 1, 1));967editor.trigger('keyboard', Handler.Type, { text: 'a' });968969}, event => {970assert.strictEqual(event.triggerOptions.auto, true);971assert.strictEqual(event.completionModel.items.length, 2);972});973});974});975976test('Unexpected suggest scoring #167242', async function () {977disposables.add(registry.register('*', {978// word-based979_debugDisplayName: 'test',980provideCompletionItems(doc, pos) {981const word = doc.getWordUntilPosition(pos);982return {983suggestions: [{984kind: CompletionItemKind.Text,985label: 'pull',986insertText: 'pull',987range: new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn)988}],989};990}991}));992disposables.add(registry.register({ scheme: 'test' }, {993// JSON-based994_debugDisplayName: 'test',995provideCompletionItems(doc, pos) {996return {997suggestions: [{998kind: CompletionItemKind.Class,999label: 'git.pull',1000insertText: 'git.pull',1001range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)1002}],1003};1004},1005}));10061007return withOracle(async function (model, editor) {10081009await assertEvent(model.onDidSuggest, () => {1010editor.setValue('gi');1011editor.setSelection(new Selection(1, 3, 1, 3));1012editor.trigger('keyboard', Handler.Type, { text: 't' });10131014}, event => {1015assert.strictEqual(event.triggerOptions.auto, true);1016assert.strictEqual(event.completionModel.items.length, 1);1017assert.strictEqual(event.completionModel.items[0].textLabel, 'git.pull');1018});10191020editor.trigger('keyboard', Handler.Type, { text: '.' });10211022await assertEvent(model.onDidSuggest, () => {1023editor.trigger('keyboard', Handler.Type, { text: 'p' });10241025}, event => {1026assert.strictEqual(event.triggerOptions.auto, true);1027assert.strictEqual(event.completionModel.items.length, 1);1028assert.strictEqual(event.completionModel.items[0].textLabel, 'git.pull');1029});1030});1031});10321033test('Completion list closes unexpectedly when typing a digit after a word separator #169390', function () {10341035const requestCounts = [0, 0];10361037disposables.add(registry.register({ scheme: 'test' }, {1038_debugDisplayName: 'test',10391040provideCompletionItems(doc, pos) {1041requestCounts[0] += 1;1042return {1043suggestions: [{1044kind: CompletionItemKind.Text,1045label: 'foo-20',1046insertText: 'foo-20',1047range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)1048}, {1049kind: CompletionItemKind.Text,1050label: 'foo-hello',1051insertText: 'foo-hello',1052range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)1053}],1054};1055}1056}));1057disposables.add(registry.register({ scheme: 'test' }, {1058_debugDisplayName: 'test',1059triggerCharacters: ['2'],1060provideCompletionItems(doc, pos, ctx) {1061requestCounts[1] += 1;1062if (ctx.triggerKind !== CompletionTriggerKind.TriggerCharacter) {1063return;1064}1065return {1066suggestions: [{1067kind: CompletionItemKind.Class,1068label: 'foo-210',1069insertText: 'foo-210',1070range: new Range(pos.lineNumber, 1, pos.lineNumber, pos.column)1071}],1072};1073},1074}));10751076return withOracle(async function (model, editor) {10771078await assertEvent(model.onDidSuggest, () => {1079editor.setValue('foo');1080editor.setSelection(new Selection(1, 4, 1, 4));1081model.trigger({ auto: false });10821083}, event => {1084assert.strictEqual(event.triggerOptions.auto, false);1085assert.strictEqual(event.completionModel.items.length, 2);1086assert.strictEqual(event.completionModel.items[0].textLabel, 'foo-20');1087assert.strictEqual(event.completionModel.items[1].textLabel, 'foo-hello');1088});10891090editor.trigger('keyboard', Handler.Type, { text: '-' });109110921093await assertEvent(model.onDidSuggest, () => {1094editor.trigger('keyboard', Handler.Type, { text: '2' });10951096}, event => {1097assert.strictEqual(event.triggerOptions.auto, true);1098assert.strictEqual(event.completionModel.items.length, 2);1099assert.strictEqual(event.completionModel.items[0].textLabel, 'foo-20');1100assert.strictEqual(event.completionModel.items[1].textLabel, 'foo-210');1101assert.deepStrictEqual(requestCounts, [1, 2]);1102});1103});1104});11051106test('Set refilter-flag, keep triggerKind', function () {11071108disposables.add(registry.register({ scheme: 'test' }, {1109_debugDisplayName: 'test',1110triggerCharacters: ['.'],1111provideCompletionItems(doc, pos, ctx) {1112return {1113suggestions: [{1114label: doc.getWordUntilPosition(pos).word || 'hello',1115kind: CompletionItemKind.Property,1116insertText: 'foofoo',1117range: getDefaultSuggestRange(doc, pos)1118}]1119};1120},1121}));11221123return withOracle(async function (model, editor) {11241125await assertEvent(model.onDidSuggest, () => {1126editor.setValue('foo');1127editor.setSelection(new Selection(1, 4, 1, 4));1128editor.trigger('keyboard', Handler.Type, { text: 'o' });112911301131}, event => {1132assert.strictEqual(event.triggerOptions.auto, true);1133assert.strictEqual(event.triggerOptions.triggerCharacter, undefined);1134assert.strictEqual(event.triggerOptions.triggerKind, undefined);1135assert.strictEqual(event.completionModel.items.length, 1);1136});11371138await assertEvent(model.onDidSuggest, () => {1139editor.trigger('keyboard', Handler.Type, { text: '.' });11401141}, event => {1142assert.strictEqual(event.triggerOptions.auto, true);1143assert.strictEqual(event.triggerOptions.refilter, undefined);1144assert.strictEqual(event.triggerOptions.triggerCharacter, '.');1145assert.strictEqual(event.triggerOptions.triggerKind, CompletionTriggerKind.TriggerCharacter);1146assert.strictEqual(event.completionModel.items.length, 1);1147});11481149await assertEvent(model.onDidSuggest, () => {1150editor.trigger('keyboard', Handler.Type, { text: 'h' });11511152}, event => {1153assert.strictEqual(event.triggerOptions.auto, true);1154assert.strictEqual(event.triggerOptions.refilter, true);1155assert.strictEqual(event.triggerOptions.triggerCharacter, '.');1156assert.strictEqual(event.triggerOptions.triggerKind, CompletionTriggerKind.TriggerCharacter);1157assert.strictEqual(event.completionModel.items.length, 1);1158});1159});1160});11611162test('Snippets gone from IntelliSense #173244', function () {11631164const snippetProvider: CompletionItemProvider = {1165_debugDisplayName: 'test',1166provideCompletionItems(doc, pos, ctx) {1167return {1168suggestions: [{1169label: 'log',1170kind: CompletionItemKind.Snippet,1171insertText: 'log',1172range: getDefaultSuggestRange(doc, pos)1173}]1174};1175}1176};1177const old = setSnippetSuggestSupport(snippetProvider);11781179disposables.add(toDisposable(() => {1180if (getSnippetSuggestSupport() === snippetProvider) {1181setSnippetSuggestSupport(old);1182}1183}));11841185disposables.add(registry.register({ scheme: 'test' }, {1186_debugDisplayName: 'test',1187triggerCharacters: ['.'],1188provideCompletionItems(doc, pos, ctx) {1189return {1190suggestions: [{1191label: 'locals',1192kind: CompletionItemKind.Property,1193insertText: 'locals',1194range: getDefaultSuggestRange(doc, pos)1195}],1196incomplete: true1197};1198},1199}));12001201return withOracle(async function (model, editor) {12021203await assertEvent(model.onDidSuggest, () => {1204editor.setValue('');1205editor.setSelection(new Selection(1, 1, 1, 1));1206editor.trigger('keyboard', Handler.Type, { text: 'l' });120712081209}, event => {1210assert.strictEqual(event.triggerOptions.auto, true);1211assert.strictEqual(event.triggerOptions.triggerCharacter, undefined);1212assert.strictEqual(event.triggerOptions.triggerKind, undefined);1213assert.strictEqual(event.completionModel.items.length, 2);1214assert.strictEqual(event.completionModel.items[0].textLabel, 'locals');1215assert.strictEqual(event.completionModel.items[1].textLabel, 'log');1216});12171218await assertEvent(model.onDidSuggest, () => {1219editor.trigger('keyboard', Handler.Type, { text: 'o' });12201221}, event => {1222assert.strictEqual(event.triggerOptions.triggerKind, CompletionTriggerKind.TriggerForIncompleteCompletions);1223assert.strictEqual(event.triggerOptions.auto, true);1224assert.strictEqual(event.completionModel.items.length, 2);1225assert.strictEqual(event.completionModel.items[0].textLabel, 'locals');1226assert.strictEqual(event.completionModel.items[1].textLabel, 'log');1227});12281229});1230});12311232test('offWhenInlineCompletions - suppresses quick suggest when inline provider exists', function () {12331234disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));12351236// Register a dummy inline completions provider1237const inlineProvider: InlineCompletionsProvider = {1238provideInlineCompletions: () => ({ items: [] }),1239disposeInlineCompletions: () => { }1240};1241disposables.add(languageFeaturesService.inlineCompletionsProvider.register({ scheme: 'test' }, inlineProvider));12421243return withOracle((suggestOracle, editor) => {1244editor.updateOptions({ quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' } });12451246return new Promise<void>((resolve, reject) => {1247const unexpectedSuggestSub = suggestOracle.onDidSuggest(() => {1248unexpectedSuggestSub.dispose();1249reject(new Error('Quick suggestions should not have been triggered'));1250});12511252editor.setPosition({ lineNumber: 1, column: 4 });1253editor.trigger('keyboard', Handler.Type, { text: 'd' });12541255// Wait for the quick suggest delay to pass without triggering1256setTimeout(() => {1257unexpectedSuggestSub.dispose();1258resolve();1259}, 200);1260});1261});1262});12631264test('offWhenInlineCompletions - allows quick suggest when no inline provider exists', function () {12651266disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));12671268// No inline completions provider registered for 'test' scheme12691270return withOracle((suggestOracle, editor) => {1271editor.updateOptions({ quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' } });12721273return assertEvent(suggestOracle.onDidSuggest, () => {1274editor.setPosition({ lineNumber: 1, column: 4 });1275editor.trigger('keyboard', Handler.Type, { text: 'd' });1276}, suggestEvent => {1277assert.strictEqual(suggestEvent.triggerOptions.auto, true);1278assert.strictEqual(suggestEvent.completionModel.items.length, 1);1279});1280});1281});12821283test('offWhenInlineCompletions - allows quick suggest when inlineSuggest is disabled', function () {1284return runWithFakedTimers({ useFakeTimers: true }, () => {1285disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));12861287// Register a dummy inline completions provider1288const inlineProvider: InlineCompletionsProvider = {1289provideInlineCompletions: () => ({ items: [] }),1290disposeInlineCompletions: () => { }1291};1292disposables.add(languageFeaturesService.inlineCompletionsProvider.register({ scheme: 'test' }, inlineProvider));12931294return withOracle((suggestOracle, editor) => {1295editor.updateOptions({1296quickSuggestions: { comments: 'off', strings: 'off', other: 'offWhenInlineCompletions' },1297inlineSuggest: { enabled: false }1298});12991300return assertEvent(suggestOracle.onDidSuggest, () => {1301editor.setPosition({ lineNumber: 1, column: 4 });1302editor.trigger('keyboard', Handler.Type, { text: 'd' });1303}, suggestEvent => {1304assert.strictEqual(suggestEvent.triggerOptions.auto, true);1305assert.strictEqual(suggestEvent.completionModel.items.length, 1);1306});1307});1308});1309});13101311test('string shorthand - "off" disables quick suggestions for all token types', function () {1312return runWithFakedTimers({ useFakeTimers: true }, () => {13131314disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));13151316return withOracle((suggestOracle, editor) => {1317// Use string shorthand instead of object form1318editor.updateOptions({ quickSuggestions: 'off' });13191320return new Promise<void>((resolve, reject) => {1321const sub = suggestOracle.onDidSuggest(() => {1322sub.dispose();1323reject(new Error('Quick suggestions should have been suppressed by string shorthand "off"'));1324});13251326editor.setPosition({ lineNumber: 1, column: 4 });1327editor.trigger('keyboard', Handler.Type, { text: 'd' });13281329setTimeout(() => {1330sub.dispose();1331resolve();1332}, 200);1333});1334});1335});1336});13371338test('string shorthand - "offWhenInlineCompletions" suppresses when inline provider exists', function () {1339return runWithFakedTimers({ useFakeTimers: true }, () => {1340disposables.add(registry.register({ scheme: 'test' }, alwaysSomethingSupport));13411342const inlineProvider: InlineCompletionsProvider = {1343provideInlineCompletions: () => ({ items: [] }),1344disposeInlineCompletions: () => { }1345};1346disposables.add(languageFeaturesService.inlineCompletionsProvider.register({ scheme: 'test' }, inlineProvider));13471348return withOracle((suggestOracle, editor) => {1349// Use string shorthand — applies to all token types1350editor.updateOptions({ quickSuggestions: 'offWhenInlineCompletions' });13511352return new Promise<void>((resolve, reject) => {1353const sub = suggestOracle.onDidSuggest(() => {1354sub.dispose();1355reject(new Error('Quick suggestions should have been suppressed by offWhenInlineCompletions shorthand'));1356});13571358editor.setPosition({ lineNumber: 1, column: 4 });1359editor.trigger('keyboard', Handler.Type, { text: 'd' });13601361setTimeout(() => {1362sub.dispose();1363resolve();1364}, 200);1365});1366});1367});1368});1369});137013711372