feat(*): one task editor, vim navigation and undo
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { TFile } from "obsidian";
|
||||
import type { App } from "obsidian";
|
||||
import { depth, forget, record, redo, transaction, undo } from "../src/vault/history";
|
||||
|
||||
/** Obsidian hands out TFile instances; history checks for them by identity. */
|
||||
function handle(path: string): TFile {
|
||||
return Object.assign(Object.create(TFile.prototype) as TFile, { path });
|
||||
}
|
||||
|
||||
function vault(seed: Record<string, string>) {
|
||||
const files = new Map(Object.entries(seed));
|
||||
const handles = new Map([...files.keys()].map((path) => [path, handle(path)]));
|
||||
const app = {
|
||||
vault: {
|
||||
getAbstractFileByPath: (path: string) => handles.get(path) ?? null,
|
||||
read: async (file: TFile) => files.get(file.path) ?? "",
|
||||
process: async (file: TFile, fn: (data: string) => string) => {
|
||||
const next = fn(files.get(file.path) ?? "");
|
||||
files.set(file.path, next);
|
||||
return next;
|
||||
},
|
||||
},
|
||||
} as unknown as App;
|
||||
return { app, files };
|
||||
}
|
||||
|
||||
/** Mirrors what mutate.edit does: write the file, then log both sides. */
|
||||
async function write(
|
||||
{ app, files }: ReturnType<typeof vault>,
|
||||
path: string,
|
||||
next: string,
|
||||
): Promise<void> {
|
||||
const before = files.get(path) ?? "";
|
||||
await app.vault.process(
|
||||
app.vault.getAbstractFileByPath(path) as TFile,
|
||||
() => next,
|
||||
);
|
||||
record(path, before, next);
|
||||
}
|
||||
|
||||
describe("history", () => {
|
||||
beforeEach(() => forget());
|
||||
|
||||
it("puts a single change back", async () => {
|
||||
const store = vault({ "a.md": "one" });
|
||||
await write(store, "a.md", "two");
|
||||
await undo(store.app);
|
||||
expect(store.files.get("a.md")).toBe("one");
|
||||
});
|
||||
|
||||
it("replays what it undid", async () => {
|
||||
const store = vault({ "a.md": "one" });
|
||||
await write(store, "a.md", "two");
|
||||
await undo(store.app);
|
||||
await redo(store.app);
|
||||
expect(store.files.get("a.md")).toBe("two");
|
||||
});
|
||||
|
||||
it("walks back through several changes one at a time", async () => {
|
||||
const store = vault({ "a.md": "one" });
|
||||
await write(store, "a.md", "two");
|
||||
await write(store, "a.md", "three");
|
||||
await undo(store.app);
|
||||
expect(store.files.get("a.md")).toBe("two");
|
||||
await undo(store.app);
|
||||
expect(store.files.get("a.md")).toBe("one");
|
||||
});
|
||||
|
||||
it("takes both files of a move back together", async () => {
|
||||
const store = vault({ "a.md": "task", "b.md": "" });
|
||||
await transaction("the move", async () => {
|
||||
await write(store, "b.md", "task");
|
||||
await write(store, "a.md", "");
|
||||
});
|
||||
expect(depth().past).toBe(1);
|
||||
await undo(store.app);
|
||||
expect(store.files.get("a.md")).toBe("task");
|
||||
expect(store.files.get("b.md")).toBe("");
|
||||
});
|
||||
|
||||
it("ignores a write that changed nothing", async () => {
|
||||
const store = vault({ "a.md": "one" });
|
||||
await write(store, "a.md", "one");
|
||||
expect(depth().past).toBe(0);
|
||||
});
|
||||
|
||||
it("refuses to touch a file that moved on underneath it", async () => {
|
||||
const store = vault({ "a.md": "one" });
|
||||
await write(store, "a.md", "two");
|
||||
store.files.set("a.md", "edited elsewhere");
|
||||
await undo(store.app);
|
||||
expect(store.files.get("a.md")).toBe("edited elsewhere");
|
||||
// The step is kept, so a later undo can still work if the file comes back.
|
||||
expect(depth().past).toBe(1);
|
||||
});
|
||||
|
||||
it("drops the redo trail once something new is written", async () => {
|
||||
const store = vault({ "a.md": "one" });
|
||||
await write(store, "a.md", "two");
|
||||
await undo(store.app);
|
||||
expect(depth().future).toBe(1);
|
||||
await write(store, "a.md", "three");
|
||||
expect(depth().future).toBe(0);
|
||||
});
|
||||
|
||||
it("says so instead of throwing when there is nothing left", async () => {
|
||||
const store = vault({ "a.md": "one" });
|
||||
await expect(undo(store.app)).resolves.toBeUndefined();
|
||||
await expect(redo(store.app)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user