mirror of
https://github.com/Silverquark/dance.git
synced 2024-09-11 10:45:43 +03:00
history: add utils and initial tests for history replaying
This commit is contained in:
parent
8ed6c65076
commit
a844ede39b
@ -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
150
test/suite/history.test.ts
Normal 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);
|
||||
// });
|
||||
});
|
Loading…
Reference in New Issue
Block a user