Path: blob/main/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts
4798 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 { timeout } from '../../../../../base/common/async.js';6import { Event } from '../../../../../base/common/event.js';7import { DisposableStore } from '../../../../../base/common/lifecycle.js';8import { mock } from '../../../../../base/test/common/mock.js';9import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';10import { Range } from '../../../../common/core/range.js';11import { CompletionItemKind, CompletionItemProvider } from '../../../../common/languages.js';12import { IEditorWorkerService } from '../../../../common/services/editorWorker.js';13import { ViewModel } from '../../../../common/viewModel/viewModelImpl.js';14import { GhostTextContext } from './utils.js';15import { SnippetController2 } from '../../../snippet/browser/snippetController2.js';16import { SuggestController } from '../../../suggest/browser/suggestController.js';17import { ISuggestMemoryService } from '../../../suggest/browser/suggestMemory.js';18import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';19import { IMenu, IMenuService } from '../../../../../platform/actions/common/actions.js';20import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';21import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';22import { MockKeybindingService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';23import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';24import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';25import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';26import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';27import assert from 'assert';28import { ILabelService } from '../../../../../platform/label/common/label.js';29import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';30import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';31import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';32import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js';33import { InlineCompletionsController } from '../../browser/controller/inlineCompletionsController.js';34import { autorun } from '../../../../../base/common/observable.js';35import { setUnexpectedErrorHandler } from '../../../../../base/common/errors.js';36import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';37import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';38import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';39import { ModifierKeyEmitter } from '../../../../../base/browser/dom.js';40import { InlineSuggestionsView } from '../../browser/view/inlineSuggestionsView.js';4142suite('Suggest Widget Model', () => {43ensureNoDisposablesAreLeakedInTestSuite();4445setup(() => {46setUnexpectedErrorHandler(function (err) {47throw err;48});49});5051// This test is skipped because the fix for this causes https://github.com/microsoft/vscode/issues/16602352test.skip('Active', async () => {53await withAsyncTestCodeEditorAndInlineCompletionsModel('',54{ fakeClock: true, provider, },55async ({ editor, editorViewModel, context, model }) => {56let last: boolean | undefined = undefined;57const history = new Array<boolean>();58const d = autorun(reader => {59/** @description debug */60const selectedSuggestItem = !!model.debugGetSelectedSuggestItem().read(reader);61if (last !== selectedSuggestItem) {62last = selectedSuggestItem;63history.push(last);64}65});6667context.keyboardType('h');68const suggestController = (editor.getContribution(SuggestController.ID) as SuggestController);69suggestController.triggerSuggest();70await timeout(1000);71assert.deepStrictEqual(history.splice(0), [false, true]);7273context.keyboardType('.');74await timeout(1000);7576// No flicker here77assert.deepStrictEqual(history.splice(0), []);78suggestController.cancelSuggestWidget();79await timeout(1000);8081assert.deepStrictEqual(history.splice(0), [false]);8283d.dispose();84}85);86});8788test('Ghost Text', async () => {89await withAsyncTestCodeEditorAndInlineCompletionsModel('',90{ fakeClock: true, provider, suggest: { preview: true } },91async ({ editor, editorViewModel, context, model }) => {92context.keyboardType('h');93const suggestController = (editor.getContribution(SuggestController.ID) as SuggestController);94suggestController.triggerSuggest();95await timeout(1000);96assert.deepStrictEqual(context.getAndClearViewStates(), ['', 'h[ello]']);9798context.keyboardType('.');99await timeout(1000);100assert.deepStrictEqual(context.getAndClearViewStates(), ['h', 'hello.[hello]']);101102suggestController.cancelSuggestWidget();103104await timeout(1000);105assert.deepStrictEqual(context.getAndClearViewStates(), ['hello.']);106}107);108});109});110111const provider: CompletionItemProvider = {112_debugDisplayName: 'test',113triggerCharacters: ['.'],114async provideCompletionItems(model, pos) {115const word = model.getWordAtPosition(pos);116const range = word117? { startLineNumber: 1, startColumn: word.startColumn, endLineNumber: 1, endColumn: word.endColumn }118: Range.fromPositions(pos);119120return {121suggestions: [{122insertText: 'hello',123kind: CompletionItemKind.Text,124label: 'hello',125range,126commitCharacters: ['.'],127}]128};129},130};131132async function withAsyncTestCodeEditorAndInlineCompletionsModel(133text: string,134options: TestCodeEditorInstantiationOptions & { provider?: CompletionItemProvider; fakeClock?: boolean; serviceCollection?: never },135callback: (args: { editor: ITestCodeEditor; editorViewModel: ViewModel; model: InlineCompletionsModel; context: GhostTextContext }) => Promise<void>136): Promise<void> {137await runWithFakedTimers({ useFakeTimers: options.fakeClock }, async () => {138const disposableStore = new DisposableStore();139140try {141const serviceCollection = new ServiceCollection(142[ITelemetryService, NullTelemetryService],143[ILogService, new NullLogService()],144[IStorageService, disposableStore.add(new InMemoryStorageService())],145[IKeybindingService, new MockKeybindingService()],146[IEditorWorkerService, new class extends mock<IEditorWorkerService>() {147override computeWordRanges() {148return Promise.resolve({});149}150}],151[ISuggestMemoryService, new class extends mock<ISuggestMemoryService>() {152override memorize(): void { }153override select(): number { return 0; }154}],155[IMenuService, new class extends mock<IMenuService>() {156override createMenu() {157return new class extends mock<IMenu>() {158override onDidChange = Event.None;159override dispose() { }160};161}162}],163[ILabelService, new class extends mock<ILabelService>() { }],164[IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { }],165// eslint-disable-next-line local/code-no-any-casts166[IAccessibilitySignalService, {167playSignal: async () => { },168isSoundEnabled(signal: unknown) { return false; },169} as any],170[IDefaultAccountService, new class extends mock<IDefaultAccountService>() {171override onDidChangeDefaultAccount = Event.None;172override getDefaultAccount = async () => null;173override setDefaultAccount = () => { };174}],175);176177if (options.provider) {178const languageFeaturesService = new LanguageFeaturesService();179serviceCollection.set(ILanguageFeaturesService, languageFeaturesService);180disposableStore.add(languageFeaturesService.completionProvider.register({ pattern: '**' }, options.provider));181}182183await withAsyncTestCodeEditor(text, { ...options, serviceCollection }, async (editor, editorViewModel, instantiationService) => {184instantiationService.stubInstance(InlineSuggestionsView, {185dispose: () => { }186});187editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2);188editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController);189editor.registerAndInstantiateContribution(InlineCompletionsController.ID, InlineCompletionsController);190const model = InlineCompletionsController.get(editor)?.model.get()!;191192const context = new GhostTextContext(model, editor);193await callback({ editor, editorViewModel, model, context });194context.dispose();195});196} finally {197disposableStore.dispose();198ModifierKeyEmitter.disposeInstance();199}200});201}202203204