140 lines
5.4 KiB
TypeScript
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+}");
|
|
});
|
|
});
|