Path: blob/main/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts
3296 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*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { promiseWithResolvers } from '../../../../../base/common/async.js';7import { CancellationToken } from '../../../../../base/common/cancellation.js';8import { DisposableStore } from '../../../../../base/common/lifecycle.js';9import { URI } from '../../../../../base/common/uri.js';10import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';11import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';12import { Position } from '../../../../common/core/position.js';13import { Handler } from '../../../../common/editorCommon.js';14import { LanguageFeatureRegistry } from '../../../../common/languageFeatureRegistry.js';15import * as languages from '../../../../common/languages.js';16import { ITextModel } from '../../../../common/model.js';17import { ParameterHintsModel } from '../../browser/parameterHintsModel.js';18import { createTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';19import { createTextModel } from '../../../../test/common/testTextModel.js';20import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';21import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';22import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';23import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';2425const mockFile = URI.parse('test:somefile.ttt');26const mockFileSelector = { scheme: 'test' };272829const emptySigHelp: languages.SignatureHelp = {30signatures: [{31label: 'none',32parameters: []33}],34activeParameter: 0,35activeSignature: 036};3738const emptySigHelpResult: languages.SignatureHelpResult = {39value: emptySigHelp,40dispose: () => { }41};4243suite('ParameterHintsModel', () => {44const disposables = new DisposableStore();45let registry: LanguageFeatureRegistry<languages.SignatureHelpProvider>;4647setup(() => {48disposables.clear();49registry = new LanguageFeatureRegistry<languages.SignatureHelpProvider>();50});5152teardown(() => {53disposables.clear();54});5556ensureNoDisposablesAreLeakedInTestSuite();5758function createMockEditor(fileContents: string) {59const textModel = disposables.add(createTextModel(fileContents, undefined, undefined, mockFile));60const editor = disposables.add(createTestCodeEditor(textModel, {61serviceCollection: new ServiceCollection(62[ITelemetryService, NullTelemetryService],63[IStorageService, disposables.add(new InMemoryStorageService())]64)65}));66return editor;67}6869function getNextHint(model: ParameterHintsModel) {70return new Promise<languages.SignatureHelpResult | undefined>(resolve => {71const sub = disposables.add(model.onChangedHints(e => {72sub.dispose();73return resolve(e ? { value: e, dispose: () => { } } : undefined);74}));75});76}7778test('Provider should get trigger character on type', async () => {79const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();8081const triggerChar = '(';8283const editor = createMockEditor('');84disposables.add(new ParameterHintsModel(editor, registry));8586disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {87signatureHelpTriggerCharacters = [triggerChar];88signatureHelpRetriggerCharacters = [];8990provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext) {91assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);92assert.strictEqual(context.triggerCharacter, triggerChar);93done();94return undefined;95}96}));9798await runWithFakedTimers({ useFakeTimers: true }, async () => {99editor.trigger('keyboard', Handler.Type, { text: triggerChar });100await donePromise;101});102});103104test('Provider should be retriggered if already active', async () => {105const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();106107const triggerChar = '(';108109const editor = createMockEditor('');110disposables.add(new ParameterHintsModel(editor, registry));111112let invokeCount = 0;113disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {114signatureHelpTriggerCharacters = [triggerChar];115signatureHelpRetriggerCharacters = [];116117provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {118++invokeCount;119try {120if (invokeCount === 1) {121assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);122assert.strictEqual(context.triggerCharacter, triggerChar);123assert.strictEqual(context.isRetrigger, false);124assert.strictEqual(context.activeSignatureHelp, undefined);125126// Retrigger127setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: triggerChar }), 0);128} else {129assert.strictEqual(invokeCount, 2);130assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);131assert.strictEqual(context.isRetrigger, true);132assert.strictEqual(context.triggerCharacter, triggerChar);133assert.strictEqual(context.activeSignatureHelp, emptySigHelp);134135done();136}137return emptySigHelpResult;138} catch (err) {139console.error(err);140throw err;141}142}143}));144145await runWithFakedTimers({ useFakeTimers: true }, async () => {146editor.trigger('keyboard', Handler.Type, { text: triggerChar });147await donePromise;148});149});150151test('Provider should not be retriggered if previous help is canceled first', async () => {152const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();153154const triggerChar = '(';155156const editor = createMockEditor('');157const hintModel = disposables.add(new ParameterHintsModel(editor, registry));158159let invokeCount = 0;160disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {161signatureHelpTriggerCharacters = [triggerChar];162signatureHelpRetriggerCharacters = [];163164provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {165try {166++invokeCount;167if (invokeCount === 1) {168assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);169assert.strictEqual(context.triggerCharacter, triggerChar);170assert.strictEqual(context.isRetrigger, false);171assert.strictEqual(context.activeSignatureHelp, undefined);172173// Cancel and retrigger174hintModel.cancel();175editor.trigger('keyboard', Handler.Type, { text: triggerChar });176} else {177assert.strictEqual(invokeCount, 2);178assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);179assert.strictEqual(context.triggerCharacter, triggerChar);180assert.strictEqual(context.isRetrigger, true);181assert.strictEqual(context.activeSignatureHelp, undefined);182done();183}184return emptySigHelpResult;185} catch (err) {186console.error(err);187throw err;188}189}190}));191192await runWithFakedTimers({ useFakeTimers: true }, () => {193editor.trigger('keyboard', Handler.Type, { text: triggerChar });194return donePromise;195});196});197198test('Provider should get last trigger character when triggered multiple times and only be invoked once', async () => {199const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();200201const editor = createMockEditor('');202disposables.add(new ParameterHintsModel(editor, registry, 5));203204let invokeCount = 0;205disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {206signatureHelpTriggerCharacters = ['a', 'b', 'c'];207signatureHelpRetriggerCharacters = [];208209provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext) {210try {211++invokeCount;212213assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);214assert.strictEqual(context.isRetrigger, false);215assert.strictEqual(context.triggerCharacter, 'c');216217// Give some time to allow for later triggers218setTimeout(() => {219assert.strictEqual(invokeCount, 1);220221done();222}, 50);223return undefined;224} catch (err) {225console.error(err);226throw err;227}228}229}));230231await runWithFakedTimers({ useFakeTimers: true }, async () => {232editor.trigger('keyboard', Handler.Type, { text: 'a' });233editor.trigger('keyboard', Handler.Type, { text: 'b' });234editor.trigger('keyboard', Handler.Type, { text: 'c' });235236await donePromise;237});238});239240test('Provider should be retriggered if already active', async () => {241const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();242243const editor = createMockEditor('');244disposables.add(new ParameterHintsModel(editor, registry, 5));245246let invokeCount = 0;247248disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {249signatureHelpTriggerCharacters = ['a', 'b'];250signatureHelpRetriggerCharacters = [];251252provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {253try {254++invokeCount;255if (invokeCount === 1) {256assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);257assert.strictEqual(context.triggerCharacter, 'a');258259// retrigger after delay for widget to show up260setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: 'b' }), 50);261} else if (invokeCount === 2) {262assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);263assert.ok(context.isRetrigger);264assert.strictEqual(context.triggerCharacter, 'b');265done();266} else {267assert.fail('Unexpected invoke');268}269270return emptySigHelpResult;271} catch (err) {272console.error(err);273throw err;274}275}276}));277278await runWithFakedTimers({ useFakeTimers: true }, () => {279editor.trigger('keyboard', Handler.Type, { text: 'a' });280return donePromise;281});282});283284test('Should cancel existing request when new request comes in', async () => {285286const editor = createMockEditor('abc def');287const hintsModel = disposables.add(new ParameterHintsModel(editor, registry));288289let didRequestCancellationOf = -1;290let invokeCount = 0;291const longRunningProvider = new class implements languages.SignatureHelpProvider {292signatureHelpTriggerCharacters = [];293signatureHelpRetriggerCharacters = [];294295296provideSignatureHelp(_model: ITextModel, _position: Position, token: CancellationToken): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {297try {298const count = invokeCount++;299disposables.add(token.onCancellationRequested(() => { didRequestCancellationOf = count; }));300301// retrigger on first request302if (count === 0) {303hintsModel.trigger({ triggerKind: languages.SignatureHelpTriggerKind.Invoke }, 0);304}305306return new Promise<languages.SignatureHelpResult>(resolve => {307setTimeout(() => {308resolve({309value: {310signatures: [{311label: '' + count,312parameters: []313}],314activeParameter: 0,315activeSignature: 0316},317dispose: () => { }318});319}, 100);320});321} catch (err) {322console.error(err);323throw err;324}325}326};327328disposables.add(registry.register(mockFileSelector, longRunningProvider));329330await runWithFakedTimers({ useFakeTimers: true }, async () => {331332hintsModel.trigger({ triggerKind: languages.SignatureHelpTriggerKind.Invoke }, 0);333assert.strictEqual(-1, didRequestCancellationOf);334335return new Promise<void>((resolve, reject) =>336disposables.add(hintsModel.onChangedHints(newParamterHints => {337try {338assert.strictEqual(0, didRequestCancellationOf);339assert.strictEqual('1', newParamterHints!.signatures[0].label);340resolve();341} catch (e) {342reject(e);343}344})));345});346});347348test('Provider should be retriggered by retrigger character', async () => {349const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();350351const triggerChar = 'a';352const retriggerChar = 'b';353354const editor = createMockEditor('');355disposables.add(new ParameterHintsModel(editor, registry, 5));356357let invokeCount = 0;358disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {359signatureHelpTriggerCharacters = [triggerChar];360signatureHelpRetriggerCharacters = [retriggerChar];361362provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {363try {364++invokeCount;365if (invokeCount === 1) {366assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);367assert.strictEqual(context.triggerCharacter, triggerChar);368369// retrigger after delay for widget to show up370setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: retriggerChar }), 50);371} else if (invokeCount === 2) {372assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);373assert.ok(context.isRetrigger);374assert.strictEqual(context.triggerCharacter, retriggerChar);375done();376} else {377assert.fail('Unexpected invoke');378}379380return emptySigHelpResult;381} catch (err) {382console.error(err);383throw err;384}385}386}));387388await runWithFakedTimers({ useFakeTimers: true }, async () => {389// This should not trigger anything390editor.trigger('keyboard', Handler.Type, { text: retriggerChar });391392// But a trigger character should393editor.trigger('keyboard', Handler.Type, { text: triggerChar });394395return donePromise;396});397});398399test('should use first result from multiple providers', async () => {400const triggerChar = 'a';401const firstProviderId = 'firstProvider';402const secondProviderId = 'secondProvider';403const paramterLabel = 'parameter';404405const editor = createMockEditor('');406const model = disposables.add(new ParameterHintsModel(editor, registry, 5));407408disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {409signatureHelpTriggerCharacters = [triggerChar];410signatureHelpRetriggerCharacters = [];411412async provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): Promise<languages.SignatureHelpResult | undefined> {413try {414if (!context.isRetrigger) {415// retrigger after delay for widget to show up416setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: triggerChar }), 50);417418return {419value: {420activeParameter: 0,421activeSignature: 0,422signatures: [{423label: firstProviderId,424parameters: [425{ label: paramterLabel }426]427}]428},429dispose: () => { }430};431}432433return undefined;434} catch (err) {435console.error(err);436throw err;437}438}439}));440441disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {442signatureHelpTriggerCharacters = [triggerChar];443signatureHelpRetriggerCharacters = [];444445async provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): Promise<languages.SignatureHelpResult | undefined> {446if (context.isRetrigger) {447return {448value: {449activeParameter: 0,450activeSignature: context.activeSignatureHelp ? context.activeSignatureHelp.activeSignature + 1 : 0,451signatures: [{452label: secondProviderId,453parameters: context.activeSignatureHelp ? context.activeSignatureHelp.signatures[0].parameters : []454}]455},456dispose: () => { }457};458}459460return undefined;461}462}));463464await runWithFakedTimers({ useFakeTimers: true }, async () => {465editor.trigger('keyboard', Handler.Type, { text: triggerChar });466467const firstHint = (await getNextHint(model))!.value;468assert.strictEqual(firstHint.signatures[0].label, firstProviderId);469assert.strictEqual(firstHint.activeSignature, 0);470assert.strictEqual(firstHint.signatures[0].parameters[0].label, paramterLabel);471472const secondHint = (await getNextHint(model))!.value;473assert.strictEqual(secondHint.signatures[0].label, secondProviderId);474assert.strictEqual(secondHint.activeSignature, 1);475assert.strictEqual(secondHint.signatures[0].parameters[0].label, paramterLabel);476});477});478479test('Quick typing should use the first trigger character', async () => {480const editor = createMockEditor('');481const model = disposables.add(new ParameterHintsModel(editor, registry, 50));482483const triggerCharacter = 'a';484485let invokeCount = 0;486disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {487signatureHelpTriggerCharacters = [triggerCharacter];488signatureHelpRetriggerCharacters = [];489490provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): languages.SignatureHelpResult | Promise<languages.SignatureHelpResult> {491try {492++invokeCount;493494if (invokeCount === 1) {495assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);496assert.strictEqual(context.triggerCharacter, triggerCharacter);497} else {498assert.fail('Unexpected invoke');499}500501return emptySigHelpResult;502} catch (err) {503console.error(err);504throw err;505}506}507}));508509await runWithFakedTimers({ useFakeTimers: true }, async () => {510editor.trigger('keyboard', Handler.Type, { text: triggerCharacter });511editor.trigger('keyboard', Handler.Type, { text: 'x' });512513await getNextHint(model);514});515});516517test('Retrigger while a pending resolve is still going on should preserve last active signature #96702', async () => {518const { promise: donePromise, resolve: done } = promiseWithResolvers<void>();519520const editor = createMockEditor('');521const model = disposables.add(new ParameterHintsModel(editor, registry, 50));522523const triggerCharacter = 'a';524const retriggerCharacter = 'b';525526let invokeCount = 0;527disposables.add(registry.register(mockFileSelector, new class implements languages.SignatureHelpProvider {528signatureHelpTriggerCharacters = [triggerCharacter];529signatureHelpRetriggerCharacters = [retriggerCharacter];530531async provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: languages.SignatureHelpContext): Promise<languages.SignatureHelpResult> {532try {533++invokeCount;534535if (invokeCount === 1) {536assert.strictEqual(context.triggerKind, languages.SignatureHelpTriggerKind.TriggerCharacter);537assert.strictEqual(context.triggerCharacter, triggerCharacter);538setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: retriggerCharacter }), 50);539} else if (invokeCount === 2) {540// Trigger again while we wait for resolve to take place541setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: retriggerCharacter }), 50);542await new Promise(resolve => setTimeout(resolve, 1000));543} else if (invokeCount === 3) {544// Make sure that in a retrigger during a pending resolve, we still have the old active signature.545assert.strictEqual(context.activeSignatureHelp, emptySigHelp);546done();547} else {548assert.fail('Unexpected invoke');549}550551return emptySigHelpResult;552} catch (err) {553console.error(err);554done(err);555throw err;556}557}558}));559560await runWithFakedTimers({ useFakeTimers: true }, async () => {561562editor.trigger('keyboard', Handler.Type, { text: triggerCharacter });563564await getNextHint(model);565await getNextHint(model);566567await donePromise;568});569});570});571572573