Files
gites/src/lib/diff.test.ts
T

140 lines
5.4 KiB
TypeScript

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('<section class="preview">', '<section class="preview" @keydown="onKey">')!)).toBe(
'<section class="preview"{+ @keydown="onKey"+}>',
);
});
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+}");
});
});