Path: blob/main/src/vs/editor/contrib/inlineCompletions/test/browser/utils.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*--------------------------------------------------------------------------------------------*/45import { timeout } from '../../../../../base/common/async.js';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { Disposable, DisposableStore, IReference } 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 { IInlineCompletionChangeHint, 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 { Emitter, Event } from '../../../../../base/common/event.js';30import { IRenameSymbolTrackerService, NullRenameSymbolTrackerService } from '../../../../browser/services/renameSymbolTrackerService.js';31import { ITextModelService, IResolvedTextEditorModel } from '../../../../common/services/resolverService.js';32import { IModelService } from '../../../../common/services/model.js';33import { URI } from '../../../../../base/common/uri.js';34import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';3536export class MockInlineCompletionsProvider implements InlineCompletionsProvider {37private returnValue: InlineCompletion[] = [];38private delayMs: number = 0;3940private callHistory = new Array<unknown>();41private calledTwiceIn50Ms = false;4243private readonly _onDidChangeEmitter = new Emitter<IInlineCompletionChangeHint | void>();44public readonly onDidChangeInlineCompletions: Event<IInlineCompletionChangeHint | void> = this._onDidChangeEmitter.event;4546constructor(47public readonly enableForwardStability = false,48) { }4950public setReturnValue(value: InlineCompletion | undefined, delayMs: number = 0): void {51this.returnValue = value ? [value] : [];52this.delayMs = delayMs;53}5455public setReturnValues(values: InlineCompletion[], delayMs: number = 0): void {56this.returnValue = values;57this.delayMs = delayMs;58}5960public getAndClearCallHistory() {61const history = [...this.callHistory];62this.callHistory = [];63return history;64}6566public assertNotCalledTwiceWithin50ms() {67if (this.calledTwiceIn50Ms) {68throw new Error('provideInlineCompletions has been called at least twice within 50ms. This should not happen.');69}70}7172/**73* Fire an onDidChange event with an optional change hint.74*/75public fireOnDidChange(changeHint?: IInlineCompletionChangeHint): void {76this._onDidChangeEmitter.fire(changeHint);77}7879private lastTimeMs: number | undefined = undefined;8081async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {82const currentTimeMs = new Date().getTime();83if (this.lastTimeMs && currentTimeMs - this.lastTimeMs < 50) {84this.calledTwiceIn50Ms = true;85}86this.lastTimeMs = currentTimeMs;8788this.callHistory.push({89position: position.toString(),90triggerKind: context.triggerKind,91text: model.getValue(),92...(context.changeHint !== undefined ? { changeHint: context.changeHint } : {}),93});94const result = new Array<InlineCompletion>();95for (const v of this.returnValue) {96const x = { ...v };97if (!x.range) {98x.range = model.getFullModelRange();99}100result.push(x);101}102103if (this.delayMs > 0) {104await timeout(this.delayMs);105}106107return { items: result, enableForwardStability: this.enableForwardStability };108}109disposeInlineCompletions() { }110handleItemDidShow() { }111}112113export class MockSearchReplaceCompletionsProvider implements InlineCompletionsProvider {114private _map = new Map<string, string>();115116public add(search: string, replace: string): void {117this._map.set(search, replace);118}119120async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {121const text = model.getValue();122for (const [search, replace] of this._map) {123const idx = text.indexOf(search);124// replace idx...idx+text.length with replace125if (idx !== -1) {126const range = Range.fromPositions(model.getPositionAt(idx), model.getPositionAt(idx + search.length));127return {128items: [129{ range, insertText: replace, isInlineEdit: true }130]131};132}133}134return { items: [] };135}136disposeInlineCompletions() { }137handleItemDidShow() { }138}139140export class InlineEditContext extends Disposable {141public readonly prettyViewStates = new Array<string | undefined>();142143constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {144super();145146const edit = derived(reader => {147const state = model.state.read(reader);148return state ? new TextEdit(state.edits) : undefined;149});150151this._register(autorun(reader => {152/** @description update */153const e = edit.read(reader);154let view: string | undefined;155156if (e) {157view = e.toString(this.editor.getValue());158} else {159view = undefined;160}161162this.prettyViewStates.push(view);163}));164}165166public getAndClearViewStates(): (string | undefined)[] {167const arr = [...this.prettyViewStates];168this.prettyViewStates.length = 0;169return arr;170}171}172173export class GhostTextContext extends Disposable {174public readonly prettyViewStates = new Array<string | undefined>();175private _currentPrettyViewState: string | undefined;176public get currentPrettyViewState() {177return this._currentPrettyViewState;178}179180constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {181super();182183this._register(autorun(reader => {184/** @description update */185const ghostText = model.primaryGhostText.read(reader);186let view: string | undefined;187if (ghostText) {188view = ghostText.render(this.editor.getValue(), true);189} else {190view = this.editor.getValue();191}192193if (this._currentPrettyViewState !== view) {194this.prettyViewStates.push(view);195}196this._currentPrettyViewState = view;197}));198}199200public getAndClearViewStates(): (string | undefined)[] {201const arr = [...this.prettyViewStates];202this.prettyViewStates.length = 0;203return arr;204}205206public keyboardType(text: string): void {207this.editor.trigger('keyboard', 'type', { text });208}209210public cursorUp(): void {211this.editor.runCommand(CoreNavigationCommands.CursorUp, null);212}213214public cursorRight(): void {215this.editor.runCommand(CoreNavigationCommands.CursorRight, null);216}217218public cursorLeft(): void {219this.editor.runCommand(CoreNavigationCommands.CursorLeft, null);220}221222public cursorDown(): void {223this.editor.runCommand(CoreNavigationCommands.CursorDown, null);224}225226public cursorLineEnd(): void {227this.editor.runCommand(CoreNavigationCommands.CursorLineEnd, null);228}229230public leftDelete(): void {231this.editor.runCommand(CoreEditingCommands.DeleteLeft, null);232}233}234235export interface IWithAsyncTestCodeEditorAndInlineCompletionsModel {236editor: ITestCodeEditor;237editorViewModel: ViewModel;238model: InlineCompletionsModel;239context: GhostTextContext;240store: DisposableStore;241}242243export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(244text: string,245options: TestCodeEditorInstantiationOptions & { provider?: InlineCompletionsProvider; fakeClock?: boolean },246callback: (args: IWithAsyncTestCodeEditorAndInlineCompletionsModel) => Promise<T>): Promise<T> {247return await runWithFakedTimers({248useFakeTimers: options.fakeClock,249}, async () => {250const disposableStore = new DisposableStore();251252try {253if (options.provider) {254const languageFeaturesService = new LanguageFeaturesService();255if (!options.serviceCollection) {256options.serviceCollection = new ServiceCollection();257}258options.serviceCollection.set(ILanguageFeaturesService, languageFeaturesService);259// eslint-disable-next-line local/code-no-any-casts260options.serviceCollection.set(IAccessibilitySignalService, {261playSignal: async () => { },262isSoundEnabled(signal: unknown) { return false; },263} as any);264options.serviceCollection.set(IBulkEditService, {265apply: async () => { throw new Error('IBulkEditService.apply not implemented'); },266hasPreviewHandler: () => { throw new Error('IBulkEditService.hasPreviewHandler not implemented'); },267setPreviewHandler: () => { throw new Error('IBulkEditService.setPreviewHandler not implemented'); },268_serviceBrand: undefined,269});270options.serviceCollection.set(ITextModelService, new SyncDescriptor(MockTextModelService));271options.serviceCollection.set(IDefaultAccountService, {272_serviceBrand: undefined,273onDidChangeDefaultAccount: Event.None,274onDidChangePolicyData: Event.None,275policyData: null,276getDefaultAccount: async () => null,277setDefaultAccountProvider: () => { },278getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; },279refresh: async () => { return null; },280signIn: async () => { return null; },281});282options.serviceCollection.set(IRenameSymbolTrackerService, new NullRenameSymbolTrackerService());283284const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider);285disposableStore.add(d);286}287288let result: T;289await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => {290instantiationService.stubInstance(InlineSuggestionsView, {291shouldShowHoverAtViewZone: () => false,292dispose: () => { },293});294const controller = instantiationService.createInstance(InlineCompletionsController, editor);295const model = controller.model.get()!;296const context = new GhostTextContext(model, editor);297try {298result = await callback({ editor, editorViewModel, model, context, store: disposableStore });299} finally {300context.dispose();301model.dispose();302controller.dispose();303}304});305306if (options.provider instanceof MockInlineCompletionsProvider) {307options.provider.assertNotCalledTwiceWithin50ms();308}309310return result!;311} finally {312disposableStore.dispose();313}314});315}316317export class AnnotatedString {318public readonly value: string;319public readonly markers: { mark: string; idx: number }[];320321constructor(src: string, annotations: string[] = ['↓']) {322const markers = findMarkers(src, annotations);323this.value = markers.textWithoutMarkers;324this.markers = markers.results;325}326327getMarkerOffset(markerIdx = 0): number {328if (markerIdx >= this.markers.length) {329throw new BugIndicatingError(`Marker index ${markerIdx} out of bounds`);330}331return this.markers[markerIdx].idx;332}333}334335function findMarkers(text: string, markers: string[]): {336results: { mark: string; idx: number }[];337textWithoutMarkers: string;338} {339const results: { mark: string; idx: number }[] = [];340let textWithoutMarkers = '';341342markers.sort((a, b) => b.length - a.length);343344let pos = 0;345for (let i = 0; i < text.length;) {346let foundMarker = false;347for (const marker of markers) {348if (text.startsWith(marker, i)) {349results.push({ mark: marker, idx: pos });350i += marker.length;351foundMarker = true;352break;353}354}355if (!foundMarker) {356textWithoutMarkers += text[i];357pos++;358i++;359}360}361362return { results, textWithoutMarkers };363}364365export class AnnotatedText extends AnnotatedString {366private readonly _transformer = new PositionOffsetTransformer(this.value);367368getMarkerPosition(markerIdx = 0): Position {369return this._transformer.getPosition(this.getMarkerOffset(markerIdx));370}371}372373class MockTextModelService implements ITextModelService {374readonly _serviceBrand: undefined;375376constructor(377@IModelService private readonly _modelService: IModelService,378) { }379380async createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {381const model = this._modelService.getModel(resource);382if (!model) {383throw new Error(`MockTextModelService: Model not found for ${resource.toString()}`);384}385return {386object: {387textEditorModel: model,388getLanguageId: () => model.getLanguageId(),389isReadonly: () => false,390isDisposed: () => model.isDisposed(),391isResolved: () => true,392onWillDispose: model.onWillDispose,393resolve: async () => { },394createSnapshot: () => model.createSnapshot(),395dispose: () => { },396},397dispose: () => { },398};399}400401registerTextModelContentProvider(): never {402throw new Error('MockTextModelService.registerTextModelContentProvider not implemented');403}404405canHandleResource(): boolean {406return false;407}408}409410411