mirror of https://github.com/microsoft/vscode.git
Adds inline edit tests
This commit is contained in:
parent
0bfd3b35ed
commit
324d1b341b
|
@ -193,6 +193,64 @@ export class TextEdit {
|
|||
equals(other: TextEdit): boolean {
|
||||
return equals(this.edits, other.edits, (a, b) => a.equals(b));
|
||||
}
|
||||
|
||||
toString(text: AbstractText | string): string {
|
||||
if (typeof text === 'string') {
|
||||
return this.toString(new StringText(text));
|
||||
}
|
||||
|
||||
if (this.edits.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.edits.map(edit => {
|
||||
const maxLength = 10;
|
||||
const originalText = text.getValueOfRange(edit.range);
|
||||
|
||||
// Get text before the edit
|
||||
const beforeRange = Range.fromPositions(
|
||||
new Position(Math.max(1, edit.range.startLineNumber - 1), 1),
|
||||
edit.range.getStartPosition()
|
||||
);
|
||||
let beforeText = text.getValueOfRange(beforeRange);
|
||||
if (beforeText.length > maxLength) {
|
||||
beforeText = '...' + beforeText.substring(beforeText.length - maxLength);
|
||||
}
|
||||
|
||||
// Get text after the edit
|
||||
const afterRange = Range.fromPositions(
|
||||
edit.range.getEndPosition(),
|
||||
new Position(edit.range.endLineNumber + 1, 1)
|
||||
);
|
||||
let afterText = text.getValueOfRange(afterRange);
|
||||
if (afterText.length > maxLength) {
|
||||
afterText = afterText.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
// Format the replaced text
|
||||
let replacedText = originalText;
|
||||
if (replacedText.length > maxLength) {
|
||||
const halfMax = Math.floor(maxLength / 2);
|
||||
replacedText = replacedText.substring(0, halfMax) + '...' +
|
||||
replacedText.substring(replacedText.length - halfMax);
|
||||
}
|
||||
|
||||
// Format the new text
|
||||
let newText = edit.text;
|
||||
if (newText.length > maxLength) {
|
||||
const halfMax = Math.floor(maxLength / 2);
|
||||
newText = newText.substring(0, halfMax) + '...' +
|
||||
newText.substring(newText.length - halfMax);
|
||||
}
|
||||
|
||||
if (replacedText.length === 0) {
|
||||
// allow-any-unicode-next-line
|
||||
return `${beforeText}❰${newText}❱${afterText}`;
|
||||
}
|
||||
// allow-any-unicode-next-line
|
||||
return `${beforeText}❰${replacedText}↦${newText}❱${afterText}`;
|
||||
}).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleTextEdit {
|
||||
|
|
|
@ -350,4 +350,8 @@ export class InlineCompletionsController extends Disposable {
|
|||
m.jump();
|
||||
}
|
||||
}
|
||||
|
||||
public testOnlyDisableUi() {
|
||||
this._view.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { timeout } from '../../../../../base/common/async.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { AnnotatedText, InlineEditContext, IWithAsyncTestCodeEditorAndInlineCompletionsModel, MockSearchReplaceCompletionsProvider, withAsyncTestCodeEditorAndInlineCompletionsModel } from './utils.js';
|
||||
|
||||
suite('Inline Edits', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
const val = new AnnotatedText(`
|
||||
class Point {
|
||||
constructor(public x: number, public y: number) {}
|
||||
|
||||
getLength2D(): number {
|
||||
return↓ Math.sqrt(this.x * this.x + this.y * this.y↓);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
async function runTest(cb: (ctx: IWithAsyncTestCodeEditorAndInlineCompletionsModel, provider: MockSearchReplaceCompletionsProvider, view: InlineEditContext) => Promise<void>): Promise<void> {
|
||||
const provider = new MockSearchReplaceCompletionsProvider();
|
||||
await withAsyncTestCodeEditorAndInlineCompletionsModel(val.value,
|
||||
{ fakeClock: true, provider, inlineSuggest: { enabled: true } },
|
||||
async (ctx) => {
|
||||
const view = new InlineEditContext(ctx.model, ctx.editor);
|
||||
ctx.store.add(view);
|
||||
await cb(ctx, provider, view);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
test('Can Accept Inline Edit', async function () {
|
||||
await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => {
|
||||
provider.add(`getLength2D(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}`, `getLength3D(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
||||
}`);
|
||||
|
||||
await model.trigger();
|
||||
await timeout(10000);
|
||||
assert.deepStrictEqual(view.getAndClearViewStates(), ([
|
||||
undefined,
|
||||
"\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n"
|
||||
]));
|
||||
|
||||
model.accept();
|
||||
|
||||
assert.deepStrictEqual(editor.getValue(), `
|
||||
class Point {
|
||||
constructor(public x: number, public y: number) {}
|
||||
|
||||
getLength3D(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
||||
}
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('Can Type Inline Edit', async function () {
|
||||
await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => {
|
||||
provider.add(`getLength2D(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}`, `getLength3D(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
||||
}`);
|
||||
await model.trigger();
|
||||
await timeout(10000);
|
||||
assert.deepStrictEqual(view.getAndClearViewStates(), ([
|
||||
undefined,
|
||||
"\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n"
|
||||
]));
|
||||
|
||||
editor.setPosition(val.getMarkerPosition(1));
|
||||
editorViewModel.type(' + t');
|
||||
|
||||
assert.deepStrictEqual(view.getAndClearViewStates(), ([
|
||||
"\n\tget❰Length2↦Length3❱D(): numbe...\n...this.y + t❰his.z...his.z❱);\n"
|
||||
]));
|
||||
|
||||
editorViewModel.type('his.z * this.z');
|
||||
assert.deepStrictEqual(view.getAndClearViewStates(), ([
|
||||
"\n\tget❰Length2↦Length3❱D(): numbe..."
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
test('Inline Edit Stays On Unrelated Edit', async function () {
|
||||
await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => {
|
||||
provider.add(`getLength2D(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}`, `getLength3D(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
||||
}`);
|
||||
await model.trigger();
|
||||
await timeout(10000);
|
||||
assert.deepStrictEqual(view.getAndClearViewStates(), ([
|
||||
undefined,
|
||||
"\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n"
|
||||
]));
|
||||
|
||||
editor.setPosition(val.getMarkerPosition(0));
|
||||
editorViewModel.type('/* */');
|
||||
|
||||
assert.deepStrictEqual(view.getAndClearViewStates(), ([
|
||||
"\n\tget❰Length2↦Length3❱D(): numbe...\n...y * this.y❰ + th...his.z❱);\n"
|
||||
]));
|
||||
|
||||
await timeout(10000);
|
||||
assert.deepStrictEqual(view.getAndClearViewStates(), ([
|
||||
undefined
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,10 +9,10 @@ import { Disposable, DisposableStore } from '../../../../../base/common/lifecycl
|
|||
import { CoreEditingCommands, CoreNavigationCommands } from '../../../../browser/coreCommands.js';
|
||||
import { Position } from '../../../../common/core/position.js';
|
||||
import { ITextModel } from '../../../../common/model.js';
|
||||
import { InlineCompletion, InlineCompletionContext, InlineCompletionsProvider } from '../../../../common/languages.js';
|
||||
import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js';
|
||||
import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
|
||||
import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js';
|
||||
import { autorun } from '../../../../../base/common/observable.js';
|
||||
import { autorun, derived } from '../../../../../base/common/observable.js';
|
||||
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
|
||||
import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
|
||||
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
|
||||
|
@ -20,6 +20,10 @@ import { ILanguageFeaturesService } from '../../../../common/services/languageFe
|
|||
import { LanguageFeaturesService } from '../../../../common/services/languageFeaturesService.js';
|
||||
import { ViewModel } from '../../../../common/viewModel/viewModelImpl.js';
|
||||
import { InlineCompletionsController } from '../../browser/controller/inlineCompletionsController.js';
|
||||
import { Range } from '../../../../common/core/range.js';
|
||||
import { TextEdit } from '../../../../common/core/textEdit.js';
|
||||
import { BugIndicatingError } from '../../../../../base/common/errors.js';
|
||||
import { PositionOffsetTransformer } from '../../../../common/core/positionToOffset.js';
|
||||
|
||||
export class MockInlineCompletionsProvider implements InlineCompletionsProvider {
|
||||
private returnValue: InlineCompletion[] = [];
|
||||
|
@ -83,6 +87,66 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider
|
|||
handleItemDidShow() { }
|
||||
}
|
||||
|
||||
export class MockSearchReplaceCompletionsProvider implements InlineCompletionsProvider {
|
||||
private _map = new Map<string, string>();
|
||||
|
||||
public add(search: string, replace: string): void {
|
||||
this._map.set(search, replace);
|
||||
}
|
||||
|
||||
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {
|
||||
const text = model.getValue();
|
||||
for (const [search, replace] of this._map) {
|
||||
const idx = text.indexOf(search);
|
||||
// replace idx...idx+text.length with replace
|
||||
if (idx !== -1) {
|
||||
const range = Range.fromPositions(model.getPositionAt(idx), model.getPositionAt(idx + search.length));
|
||||
return {
|
||||
items: [
|
||||
{ range, insertText: replace, isInlineEdit: true }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
return { items: [] };
|
||||
}
|
||||
freeInlineCompletions() { }
|
||||
handleItemDidShow() { }
|
||||
}
|
||||
|
||||
export class InlineEditContext extends Disposable {
|
||||
public readonly prettyViewStates = new Array<string | undefined>();
|
||||
|
||||
constructor(model: InlineCompletionsModel, private readonly editor: ITestCodeEditor) {
|
||||
super();
|
||||
|
||||
const edit = derived(reader => {
|
||||
const state = model.state.read(reader);
|
||||
return state ? new TextEdit(state.edits) : undefined;
|
||||
});
|
||||
|
||||
this._register(autorun(reader => {
|
||||
/** @description update */
|
||||
const e = edit.read(reader);
|
||||
let view: string | undefined;
|
||||
|
||||
if (e) {
|
||||
view = e.toString(this.editor.getValue());
|
||||
} else {
|
||||
view = undefined;
|
||||
}
|
||||
|
||||
this.prettyViewStates.push(view);
|
||||
}));
|
||||
}
|
||||
|
||||
public getAndClearViewStates(): (string | undefined)[] {
|
||||
const arr = [...this.prettyViewStates];
|
||||
this.prettyViewStates.length = 0;
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
export class GhostTextContext extends Disposable {
|
||||
public readonly prettyViewStates = new Array<string | undefined>();
|
||||
private _currentPrettyViewState: string | undefined;
|
||||
|
@ -180,6 +244,7 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
|
|||
let result: T;
|
||||
await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => {
|
||||
const controller = instantiationService.createInstance(InlineCompletionsController, editor);
|
||||
controller.testOnlyDisableUi();
|
||||
const model = controller.model.get()!;
|
||||
const context = new GhostTextContext(model, editor);
|
||||
try {
|
||||
|
@ -201,3 +266,59 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class AnnotatedString {
|
||||
public readonly value: string;
|
||||
public readonly markers: { mark: string; idx: number }[];
|
||||
|
||||
constructor(src: string, annotations: string[] = ['↓']) {
|
||||
const markers = findMarkers(src, annotations);
|
||||
this.value = markers.textWithoutMarkers;
|
||||
this.markers = markers.results;
|
||||
}
|
||||
|
||||
getMarkerOffset(markerIdx = 0): number {
|
||||
if (markerIdx >= this.markers.length) {
|
||||
throw new BugIndicatingError(`Marker index ${markerIdx} out of bounds`);
|
||||
}
|
||||
return this.markers[markerIdx].idx;
|
||||
}
|
||||
}
|
||||
|
||||
function findMarkers(text: string, markers: string[]): {
|
||||
results: { mark: string; idx: number }[];
|
||||
textWithoutMarkers: string;
|
||||
} {
|
||||
const results: { mark: string; idx: number }[] = [];
|
||||
let textWithoutMarkers = '';
|
||||
|
||||
markers.sort((a, b) => b.length - a.length);
|
||||
|
||||
let pos = 0;
|
||||
for (let i = 0; i < text.length;) {
|
||||
let foundMarker = false;
|
||||
for (const marker of markers) {
|
||||
if (text.startsWith(marker, i)) {
|
||||
results.push({ mark: marker, idx: pos });
|
||||
i += marker.length;
|
||||
foundMarker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundMarker) {
|
||||
textWithoutMarkers += text[i];
|
||||
pos++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return { results, textWithoutMarkers };
|
||||
}
|
||||
|
||||
export class AnnotatedText extends AnnotatedString {
|
||||
private readonly _transformer = new PositionOffsetTransformer(this.value);
|
||||
|
||||
getMarkerPosition(markerIdx = 0): Position {
|
||||
return this._transformer.getPosition(this.getMarkerOffset(markerIdx));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export class TestAccessibilityService implements IAccessibilityService {
|
|||
onDidChangeReducedMotion = Event.None;
|
||||
|
||||
isScreenReaderOptimized(): boolean { return false; }
|
||||
isMotionReduced(): boolean { return false; }
|
||||
isMotionReduced(): boolean { return true; }
|
||||
alwaysUnderlineAccessKeys(): Promise<boolean> { return Promise.resolve(false); }
|
||||
setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { }
|
||||
getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; }
|
||||
|
|
Loading…
Reference in New Issue