import { describe, expect, it } from "vitest"; import { inlineRows, splitRows, unifiedRows, wordDiff, type DiffLine, type Hunk, type Row, type SplitRow } from "./diff"; /** Builds a hunk from lines written as git prints them: " same", "-old", "+new". */ function hunk(oldStart: number, newStart: number, ...lines: string[]): Hunk { return { header: `@@ -${oldStart} +${newStart} @@`, old_start: oldStart, new_start: newStart, lines: lines.map((l) => ({ kind: l[0] as DiffLine["kind"], text: l.slice(1) })), }; } /** Writes a row as text: removed parts in [-…-], added parts in {+…+}. */ const show = (pieces: { op: string; text: string }[]) => pieces.map((p) => (p.op === "-" ? `[-${p.text}-]` : p.op === "+" ? `{+${p.text}+}` : p.text)).join(""); const rowText = (r: Row) => (r.kind === "hunk" ? r.text : `${r.kind} ${r.old ?? "."} ${r.new ?? "."} ${show(r.pieces)}`); const splitText = (r: SplitRow) => r.kind === "hunk" ? r.text : `${r.left ? `${r.left.kind}${r.left.no} ${show(r.left.pieces)}` : "~"} | ${r.right ? `${r.right.kind}${r.right.no} ${show(r.right.pieces)}` : "~"}`; describe("wordDiff", () => { it("marks the words that changed", () => { expect(show(wordDiff("Pick a commit to see its message and the files", "Pick a commit to see its message, the files")!)).toBe( "Pick a commit to see its message[- and-]{+,+} the files", ); }); it("keeps added text at the end of a line", () => { expect(show(wordDiff('
', '
')!)).toBe( '
', ); }); it("gives up on lines with little in common", () => { expect(wordDiff("}", "function choose(f: ChangedFile) {")).toBeNull(); }); it("joins changed words separated only by spaces into one change", () => { expect(show(wordDiff("// settings: the sidebar width", "// settings, saved in settings.json")!)).toBe( "// settings[-: the sidebar width-]{+, saved in settings.json+}", ); }); it("treats two equal lines as unchanged", () => { expect(wordDiff("same", "same")).toEqual([{ op: "=", text: "same" }]); }); }); const change = hunk(40, 40, " keep", "-const open = ref(false);", "+const chosen = ref(null);", "+extra line", " end"); describe("unifiedRows", () => { it("numbers lines on both sides and marks changed words", () => { expect(unifiedRows([change]).map(rowText)).toEqual([ "@@ -40 +40 @@", " 40 40 keep", "- 41 . const [-open-] = ref([-false-]);", "+ . 41 const {+chosen+} = ref({+null+});", "+ . 42 extra line", " 42 43 end", ]); }); it("does not mark words in lines that were replaced by something unrelated", () => { const rows = unifiedRows([hunk(1, 1, "-}", "+function f() {")]); expect(rows.map(rowText).slice(1)).toEqual(["- 1 . }", "+ . 1 function f() {"]); }); }); describe("pairing removed and added lines", () => { const block = hunk( 1, 1, "-import type { FilterState, Ref } from types;", "+import { addRecent } from recents;", "+import type { CommitDetails, FilterState, Ref } from types;", ); it("pairs a removed line with the added line most like it", () => { expect(unifiedRows([block]).map(rowText).slice(1)).toEqual([ "- 1 . import type { FilterState, Ref } from types;", "+ . 1 import { addRecent } from recents;", "+ . 2 import type { {+CommitDetails, +}FilterState, Ref } from types;", ]); }); it("keeps the pair on one row side by side", () => { expect(splitRows([block]).map(splitText).slice(1)).toEqual([ "~ | +1 import { addRecent } from recents;", "-1 import type { FilterState, Ref } from types; | +2 import type { {+CommitDetails, +}FilterState, Ref } from types;", ]); }); it("shows the unpaired line before the pair inline", () => { expect(inlineRows([block]).map(rowText).slice(1)).toEqual([ "+ . 1 import { addRecent } from recents;", "~ 1 2 import type { {+CommitDetails, +}FilterState, Ref } from types;", ]); }); }); describe("splitRows", () => { it("puts removed and added lines side by side, with a gap where one side has no line", () => { expect(splitRows([change]).map(splitText)).toEqual([ "@@ -40 +40 @@", " 40 keep | 40 keep", "-41 const [-open-] = ref([-false-]); | +41 const {+chosen+} = ref({+null+});", "~ | +42 extra line", " 42 end | 43 end", ]); }); }); describe("inlineRows", () => { it("merges a changed line into one row and keeps added lines whole", () => { expect(inlineRows([change]).map(rowText)).toEqual([ "@@ -40 +40 @@", " 40 40 keep", "~ 41 41 const [-open-]{+chosen+} = ref([-false-]{+null+});", "+ . 42 extra line", " 42 43 end", ]); }); it("shows unrelated lines as all removed, then all added", () => { const rows = inlineRows([hunk(1, 1, "-}", "-return x;", "+function f() {", "+ call();")]); expect(rows.map(rowText).slice(1)).toEqual(["- 1 . }", "- 2 . return x;", "+ . 1 function f() {", "+ . 2 call();"]); }); }); describe("large blocks", () => { it("pairs lines by position when there are too many to compare every pair", () => { const lines = Array.from({ length: 60 }, (_, i) => `-value ${i} old`).concat(Array.from({ length: 60 }, (_, i) => `+value ${i} new`)); const rows = inlineRows([hunk(1, 1, ...lines)]); expect(rows.length).toBe(61); expect(rowText(rows[60])).toBe("~ 60 60 value 59 [-old-]{+new+}"); }); });