Path: blob/main/src/vs/editor/contrib/inlineCompletions/test/browser/utils.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 { CancellationToken } from '../../../../../base/common/cancellation.js';7import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';8import { CoreEditingCommands, CoreNavigationCommands } from '../../../../browser/coreCommands.js';9import { Position } from '../../../../common/core/position.js';10import { ITextModel } from '../../../../common/model.js';11import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js';12import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';13import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js';14import { autorun, derived } from '../../../../../base/common/observable.js';15import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';16import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';17import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';18import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';19import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';20import { ViewModel } from '../../../../common/viewModel/viewModelImpl.js';21import { InlineCompletionsController } from '../../browser/controller/inlineCompletionsController.js';22import { Range } from '../../../../common/core/range.js';23import { TextEdit } from '../../../../common/core/edits/textEdit.js';24import { BugIndicatingError } from '../../../../../base/common/errors.js';25import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js';26import { InlineSuggestionsView } from '../../browser/view/inlineSuggestionsView.js';27import { IBulkEditService } from '../../../../browser/services/bulkEditService.js';28import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';29import { Event } from '../../../../../base/common/event.js';3031export class MockInlineCompletionsProvider implements InlineCompletionsProvider {32private returnValue: InlineCompletion[] = [];33private delayMs: number = 0;3435private callHistory = new Array<unknown>();36private calledTwiceIn50Ms = false;3738constructor(39public readonly enableForwardStability = false,40) { }4142public setReturnValue(value: InlineCompletion | undefined, delayMs: number = 0): void {43this.returnValue = value ? [value] : [];44this.delayMs = delayMs;45}4647public setReturnValues(values: InlineCompletion[], delayMs: number = 0): void {48this.returnValue = values;49this.delayMs = delayMs;50}5152public getAndClearCallHistory() {53const history = [...this.callHistory];54this.callHistory = [];55return history;56}5758public assertNotCalledTwiceWithin50ms() {59if (this.calledTwiceIn50Ms) {60throw new Error('provideInlineCompletions has been called at least twice within 50ms. This should not happen.');61}62}6364private lastTimeMs: number | undefined = undefined;6566async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {67const currentTimeMs = new Date().getTime();68if (this.lastTimeMs && currentTimeMs - this.lastTimeMs < 50) {69this.calledTwiceIn50Ms = true;70}71this.lastTimeMs = currentTimeMs;7273this.callHistory.push({74position: position.toString(),75triggerKind: context.triggerKind,76text: model.getValue()77});78const result = new Array<InlineCompletion>();79for (const v of this.returnValue) {80const x = { ...v };81if (!x.range) {82x.range = model.getFullModelRange();83}84result.push(x);85}8687if (this.delayMs > 0) {88await timeout(this.delayMs);89}9091return { items: result, enableForwardStability: this.enableForwardStability };92}93disposeInlineCompletions() { }94handleItemDidShow() { }95}9697export class MockSearchReplaceCompletionsProvider implements InlineCompletionsProvider {98private _map = new Map<string, string>();99100public add(search: string, replace: string): void {101this._map.set(search, replace);102}103104async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {105const text = model.getValue();106for (const [search, replace] of this._map) {107const idx = text.indexOf(search);108// replace idx...idx+text.length with replace109if (idx !== -1) {110const range = Range.fromPositions(model.getPositionAt(idx), model.getPositionAt(idx + search.length));111return {112items: [113{ range, insertText: replace, isInlineEdit: true }114]115};116}117}118return { items: [] };119}120disposeInlineCompletions() { }121handleItemDidShow() { }122}123124export class InlineEditContext extends Disposable {125public readonly prettyViewStates = new Array<string | undefined>();126127constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {128super();129130const edit = derived(reader => {131const state = model.state.read(reader);132return state ? new TextEdit(state.edits) : undefined;133});134135this._register(autorun(reader => {136/** @description update */137const e = edit.read(reader);138let view: string | undefined;139140if (e) {141view = e.toString(this.editor.getValue());142} else {143view = undefined;144}145146this.prettyViewStates.push(view);147}));148}149150public getAndClearViewStates(): (string | undefined)[] {151const arr = [...this.prettyViewStates];152this.prettyViewStates.length = 0;153return arr;154}155}156157export class GhostTextContext extends Disposable {158public readonly prettyViewStates = new Array<string | undefined>();159private _currentPrettyViewState: string | undefined;160public get currentPrettyViewState() {161return this._currentPrettyViewState;162}163164constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {165super();166167this._register(autorun(reader => {168/** @description update */169const ghostText = model.primaryGhostText.read(reader);170let view: string | undefined;171if (ghostText) {172view = ghostText.render(this.editor.getValue(), true);173} else {174view = this.editor.getValue();175}176177if (this._currentPrettyViewState !== view) {178this.prettyViewStates.push(view);179}180this._currentPrettyViewState = view;181}));182}183184public getAndClearViewStates(): (string | undefined)[] {185const arr = [...this.prettyViewStates];186this.prettyViewStates.length = 0;187return arr;188}189190public keyboardType(text: string): void {191this.editor.trigger('keyboard', 'type', { text });192}193194public cursorUp(): void {195this.editor.runCommand(CoreNavigationCommands.CursorUp, null);196}197198public cursorRight(): void {199this.editor.runCommand(CoreNavigationCommands.CursorRight, null);200}201202public cursorLeft(): void {203this.editor.runCommand(CoreNavigationCommands.CursorLeft, null);204}205206public cursorDown(): void {207this.editor.runCommand(CoreNavigationCommands.CursorDown, null);208}209210public cursorLineEnd(): void {211this.editor.runCommand(CoreNavigationCommands.CursorLineEnd, null);212}213214public leftDelete(): void {215this.editor.runCommand(CoreEditingCommands.DeleteLeft, null);216}217}218219export interface IWithAsyncTestCodeEditorAndInlineCompletionsModel {220editor: ITestCodeEditor;221editorViewModel: ViewModel;222model: InlineCompletionsModel;223context: GhostTextContext;224store: DisposableStore;225}226227export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(228text: string,229options: TestCodeEditorInstantiationOptions & { provider?: InlineCompletionsProvider; fakeClock?: boolean },230callback: (args: IWithAsyncTestCodeEditorAndInlineCompletionsModel) => Promise<T>): Promise<T> {231return await runWithFakedTimers({232useFakeTimers: options.fakeClock,233}, async () => {234const disposableStore = new DisposableStore();235236try {237if (options.provider) {238const languageFeaturesService = new LanguageFeaturesService();239if (!options.serviceCollection) {240options.serviceCollection = new ServiceCollection();241}242options.serviceCollection.set(ILanguageFeaturesService, languageFeaturesService);243// eslint-disable-next-line local/code-no-any-casts244options.serviceCollection.set(IAccessibilitySignalService, {245playSignal: async () => { },246isSoundEnabled(signal: unknown) { return false; },247} as any);248options.serviceCollection.set(IBulkEditService, {249apply: async () => { throw new Error('IBulkEditService.apply not implemented'); },250hasPreviewHandler: () => { throw new Error('IBulkEditService.hasPreviewHandler not implemented'); },251setPreviewHandler: () => { throw new Error('IBulkEditService.setPreviewHandler not implemented'); },252_serviceBrand: undefined,253});254options.serviceCollection.set(IDefaultAccountService, {255_serviceBrand: undefined,256onDidChangeDefaultAccount: Event.None,257getDefaultAccount: async () => null,258setDefaultAccount: () => { },259});260261const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider);262disposableStore.add(d);263}264265let result: T;266await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => {267instantiationService.stubInstance(InlineSuggestionsView, {268shouldShowHoverAtViewZone: () => false,269dispose: () => { },270});271const controller = instantiationService.createInstance(InlineCompletionsController, editor);272const model = controller.model.get()!;273const context = new GhostTextContext(model, editor);274try {275result = await callback({ editor, editorViewModel, model, context, store: disposableStore });276} finally {277context.dispose();278model.dispose();279controller.dispose();280}281});282283if (options.provider instanceof MockInlineCompletionsProvider) {284options.provider.assertNotCalledTwiceWithin50ms();285}286287return result!;288} finally {289disposableStore.dispose();290}291});292}293294export class AnnotatedString {295public readonly value: string;296public readonly markers: { mark: string; idx: number }[];297298constructor(src: string, annotations: string[] = ['↓']) {299const markers = findMarkers(src, annotations);300this.value = markers.textWithoutMarkers;301this.markers = markers.results;302}303304getMarkerOffset(markerIdx = 0): number {305if (markerIdx >= this.markers.length) {306throw new BugIndicatingError(`Marker index ${markerIdx} out of bounds`);307}308return this.markers[markerIdx].idx;309}310}311312function findMarkers(text: string, markers: string[]): {313results: { mark: string; idx: number }[];314textWithoutMarkers: string;315} {316const results: { mark: string; idx: number }[] = [];317let textWithoutMarkers = '';318319markers.sort((a, b) => b.length - a.length);320321let pos = 0;322for (let i = 0; i < text.length;) {323let foundMarker = false;324for (const marker of markers) {325if (text.startsWith(marker, i)) {326results.push({ mark: marker, idx: pos });327i += marker.length;328foundMarker = true;329break;330}331}332if (!foundMarker) {333textWithoutMarkers += text[i];334pos++;335i++;336}337}338339return { results, textWithoutMarkers };340}341342export class AnnotatedText extends AnnotatedString {343private readonly _transformer = new PositionOffsetTransformer(this.value);344345getMarkerPosition(markerIdx = 0): Position {346return this._transformer.getPosition(this.getMarkerOffset(markerIdx));347}348}349350351