history: add utils and initial tests for history replaying

This commit is contained in:
Grégoire Geis 2021-10-19 22:07:58 +02:00
parent 8ed6c65076
commit a844ede39b
2 changed files with 186 additions and 12 deletions

View File

@ -81,13 +81,7 @@ export class Recorder implements vscode.Disposable {
* Returns the last 100 entries of the recorder, for debugging purposes.
*/
private get debugBuffer() {
const lastEntries = this.lastEntries(100);
for (const entry of lastEntries) {
(entry as any).debugItems = entry.items();
}
return lastEntries;
return this.lastEntries(100);
}
/**
@ -638,9 +632,16 @@ export class Recorder implements vscode.Disposable {
if (cursor.previousIs(Entry.InsertAfter)) {
// Insert -> Insert = Insert.
(cursor.buffer as Recorder.MutableBuffer)[cursor.offset + 1] =
this._storeObject(commonInsertedText);
const previousInsertedText = cursor.entry().insertedText();
if (previousInsertedText.length === commonOffsetFromActive) {
(cursor.buffer as Recorder.MutableBuffer)[cursor.offset + 1] =
this._storeObject(previousInsertedText + commonInsertedText);
} else {
this._record(Entry.InsertAfter, this._storeObject(commonInsertedText));
}
} else {
// TODO: handle offset from active
this._record(Entry.InsertAfter, this._storeObject(commonInsertedText));
}
}
@ -878,9 +879,16 @@ export class Recording {
}
/**
* Replays the recording in the given context.
* Returns the result of calling `entries()`, for debugging purposes.
*/
public async replay(context = Context.WithoutActiveEditor.current) {
private get debugEntries() {
return [...this.entries()];
}
/**
* Returns an iterator over all the entries in the recorder.
*/
public *entries(context = Context.WithoutActiveEditor.current) {
let offset = this.offset;
const buffer = this.buffer,
end = offset + this.length,
@ -889,10 +897,19 @@ export class Recording {
while (offset < end) {
const entry = recorder.entry(buffer, offset);
await entry.replay(context);
yield entry;
offset += entry.size + 1;
}
}
/**
* Replays the recording in the given context.
*/
public async replay(context = Context.WithoutActiveEditor.current) {
for (const entry of this.entries(context)) {
await entry.replay(context);
}
}
}
/**
@ -945,6 +962,13 @@ export namespace Entry {
return this.constructor as Entry.AnyClass;
}
/**
* Returns the result of calling `items()`, for debugging purposes.
*/
private get debugItems() {
return this.items();
}
/**
* Replays the recorded entry.
*/

150
test/suite/history.test.ts Normal file
View File

@ -0,0 +1,150 @@
import * as vscode from "vscode";
import { Context } from "../../src/api";
import { Extension } from "../../src/state/extension";
import { ExpectedDocument } from "./utils";
const { executeCommand } = vscode.commands;
function delay(ms = 10) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
async function type(text: string) {
await executeCommand("workbench.action.focusActiveEditorGroup");
// We can't simply use
// await vscode.commands.executeCommand("type", { text });
//
// Since such insertions happen differently from actual, keyboard based
// insertions.
//
// Keyboard based insertions emit a DidTextDocumentChange event first,
// followed by a DidEditorSelectionsChange event. `type`-based insertions emit
// DidTextDocumentChange events and DidEditorSelectionsChange events in a
// somewhat more parallel way.
//
// At the time of writing, using `type` once produces these events with "abc":
// - Text insert: "a"
// - Text insert: "b" (at an offset from "a")
// - Selection change
// - Text insert: "c"
// - Selection change
//
// Whereas keyboard-based events produce:
// - Text insert: "a"
// - Selection change
// - Text insert: "b"
// - Selection change
// - Text insert: "c"
// - Selection change
for (const char of text) {
await executeCommand("type", { text: char });
}
}
async function deleteBefore(count = 1) {
for (let i = 0; i < count; i++) {
await executeCommand("deleteLeft");
}
}
async function deleteAfter(count = 1) {
for (let i = 0; i < count; i++) {
await executeCommand("deleteRight");
}
}
suite("History tests", function () {
// Set up document.
let document: vscode.TextDocument,
editor: vscode.TextEditor,
extension: Extension;
this.beforeAll(async () => {
document = await vscode.workspace.openTextDocument();
editor = await vscode.window.showTextDocument(document);
editor.options.insertSpaces = true;
editor.options.tabSize = 2;
extension = (await vscode.extensions.getExtension("gregoire.dance")!.activate()).extension;
await new Promise<void>((resolve) => {
const disposable = extension.editors.onModeDidChange(async () => {
disposable.dispose();
await extension.editors.getState(editor).setMode(extension.modes.get("insert")!);
resolve();
});
});
});
this.afterAll(async () => {
await executeCommand("workbench.action.closeActiveEditor");
});
async function record(f: () => Promise<void>) {
const recorder = extension.recorder,
recording = recorder.startRecording();
await extension.editors.getState(editor).setMode(extension.modes.get("insert")!);
await f();
await extension.editors.getState(editor).setMode(extension.modes.get("normal")!);
await delay(10); // For VS Code to update the editor in the extension host.
return recording.complete();
}
function testRepeat(name: string, document: string, run: () => Promise<void>) {
test(name, async function () {
const startDocument = ExpectedDocument.parseIndented(4, document);
await startDocument.apply(editor);
await delay(10); // For VS Code to update the editor in the backend.
const recording = await record(run),
expectedDocument = ExpectedDocument.snapshot(editor);
await startDocument.apply(editor);
await delay(10); // For VS Code to update the editor in the backend.
await recording.replay(
new Context(extension.editors.getState(editor), extension.cancellationToken));
expectedDocument.assertEquals(editor);
});
}
testRepeat("insert a", `
foo bar
| 0
`, async () => {
await type("a");
});
testRepeat("insert abc and delete c", `
foo bar
| 0
`, async () => {
await type("abc");
await deleteBefore(1);
});
testRepeat("insert abc and delete o", `
foo bar
| 0
`, async () => {
await type("abc");
await deleteAfter(1);
});
// TODO: test does not pass
// testRepeat("insert abc, left and delete b", `
// foo bar
// | 0
// `, async () => {
// await type("abc");
// await executeCommand("dance.modes.set.normal");
// await executeCommand("dance.select.left.jump");
// await deleteBefore(1);
// });
});