Файли
openidea/src/editor/core/model/attachment.test.ts
T
ntbang0901 f752f2db08 feat(vcs): make the merge editor work like IntelliJ's merge tool (#902)
The result pane starts from the base and every change, conflicting or not, gets Accept / Ignore next to its side; accepting one side of a conflict still lets the other side be appended, and the modifier key resolves using that side directly. Adds Resolve Simple Conflicts (word-level), Apply Non-Conflicting Changes from the left, right or both (optionally on opening), Revert Conflict Resolution, Accept Yours / Accept Theirs / Cancel / Apply, Compare Contents between any two versions, and undo back to the conflicted state. CRLF and UTF-16 files are written back exactly, deleted sides and binary files are handled, and the commands are registered as merge.* actions.

Refs #826

Co-authored-by: ntbang0901 <[email protected]>
2026-11-03 23:37:38 +08:00

114 рядки
4.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { EditorView } from "@codemirror/view";
import { history, undo } from "@codemirror/commands";
import { editorsFor } from "@/editor/api";
import { createMemoryBackend } from "../backend";
import { _resetLanguages } from "../cm/languages";
import { attachDocument } from "./attachment";
import { DocumentManager, useDocMeta } from "./document";
import { EditorSlot, type SlotHost } from "./slot";
const host: SlotHost = {
onFocus: () => {},
onBlur: () => {},
onSelection: () => {},
onUserEdit: () => {},
onContextMenu: () => {},
};
async function settle() {
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
}
function setup(files: Record<string, string>) {
_resetLanguages();
const backend = createMemoryBackend(files);
const scheduled: Array<() => void> = [];
const manager = new DocumentManager(backend, { settings: () => ({}), notify: vi.fn() }, (fn) => scheduled.push(fn));
manager.start();
const frame = async () => {
scheduled.splice(0).forEach((f) => f());
await settle();
};
return { backend, manager, frame };
}
afterEach(() => useDocMeta.setState({}, true));
describe("attached document views (editable diff side)", () => {
it("can save a confirmed merge result without trimming or adding a final newline", async () => {
const { backend, manager } = setup({ "/p/merge.txt": "old\n" });
const att = await attachDocument(manager, "/p/merge.txt");
const view = new EditorView({ doc: att.text(), extensions: [att.extension] });
const model = manager.get("/p/merge.txt")!;
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "标题😀 \nexact result " } });
await att.save({ exact: true });
expect(view.state.doc.toString()).toBe("标题😀 \nexact result ");
expect(backend.docs.get(model.doc)?.text).toBe("标题😀 \nexact result ");
expect(useDocMeta.getState()["/p/merge.txt"]?.dirty).toBe(false);
view.destroy();
att.release();
});
it("shares the document with editor tabs both ways and syncs to the backend", async () => {
const { backend, manager, frame } = setup({ "/p/a.txt": "one\ntwo\n" });
const model = await manager.acquire("/p/a.txt");
const tab = new EditorSlot(model, "t1", host);
const att = await attachDocument(manager, "/p/a.txt");
expect(att.readOnly).toBe(false);
const view = new EditorView({ doc: att.text(), extensions: [att.extension, history()] });
expect(editorsFor("/p/a.txt").some((h) => h.view === view)).toBe(true);
// Typing in the diff reaches the tab and the backend.
view.dispatch({ changes: { from: 0, insert: ">" }, userEvent: "input.type" });
expect(tab.currentState().doc.toString()).toBe(">one\ntwo\n");
await frame();
expect(backend.docs.get(model.doc)?.text).toBe(">one\ntwo\n");
expect(useDocMeta.getState()["/p/a.txt"]?.dirty).toBe(true);
// Typing in the tab is mirrored into the diff, without entering its undo history.
tab.dispatchLocal({ changes: { from: 4, insert: "!" } });
expect(view.state.doc.toString()).toBe(">one!\ntwo\n");
undo(view);
expect(view.state.doc.toString()).toBe("one!\ntwo\n");
expect(tab.currentState().doc.toString()).toBe("one!\ntwo\n");
// Save goes through the document (Save All saves it too).
await att.save();
expect(useDocMeta.getState()["/p/a.txt"]?.dirty).toBe(false);
view.destroy();
att.release();
tab.dispose();
});
it("keeps the document open while attached and makes backend patches undoable without tabs", async () => {
const { backend, manager } = setup({ "/p/b.txt": "abc" });
const att = await attachDocument(manager, "/p/b.txt");
const view = new EditorView({ doc: att.text(), extensions: [att.extension, history()] });
const model = manager.get("/p/b.txt")!;
backend.patch(model.doc, [{ from: 0, to: 1, insert: "X" }]);
expect(view.state.doc.toString()).toBe("Xbc");
undo(view);
expect(view.state.doc.toString()).toBe("abc");
view.destroy();
att.release();
await settle();
expect(manager.get("/p/b.txt")).toBeUndefined();
});
it("catches up when the document changed before the view was created", async () => {
const { manager } = setup({ "/p/c.txt": "old" });
const model = await manager.acquire("/p/c.txt");
const tab = new EditorSlot(model, "t1", host);
const att = await attachDocument(manager, "/p/c.txt");
const stale = att.text();
tab.dispatchLocal({ changes: { from: 0, to: 3, insert: "new" } });
const view = new EditorView({ doc: stale, extensions: [att.extension] });
await settle();
expect(view.state.doc.toString()).toBe("new");
view.destroy();
att.release();
tab.dispose();
});
});