Diff view for a file chosen in the commit preview: one column, side by side or inline, with changed words marked
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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+}");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user