100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
// Line-based diff (git style) used by the version history diff viewer. A simple
|
|
// Longest-Common-Subsequence over lines yields a sequence of equal/added/removed
|
|
// rows that can be rendered as a unified diff or split side-by-side.
|
|
|
|
export type DiffOp = 'equal' | 'add' | 'del';
|
|
|
|
export interface DiffRow {
|
|
op: DiffOp;
|
|
// Line content. For 'equal' both sides are identical (text).
|
|
text: string;
|
|
// 1-based line numbers in the left/right document; null when absent on a side.
|
|
leftNo: number | null;
|
|
rightNo: number | null;
|
|
}
|
|
|
|
function splitLines(s: string): string[] {
|
|
// Drop a single trailing newline so a file ending in "\n" doesn't yield a
|
|
// spurious empty final row.
|
|
const t = s.endsWith('\n') ? s.slice(0, -1) : s;
|
|
return t.length === 0 ? [] : t.split('\n');
|
|
}
|
|
|
|
// diffLines computes the LCS table and walks it back into ordered rows.
|
|
export function diffLines(leftText: string, rightText: string): DiffRow[] {
|
|
const a = splitLines(leftText);
|
|
const b = splitLines(rightText);
|
|
const n = a.length;
|
|
const m = b.length;
|
|
|
|
// lcs[i][j] = length of LCS of a[i:] and b[j:].
|
|
const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
for (let i = n - 1; i >= 0; i--) {
|
|
for (let j = m - 1; j >= 0; j--) {
|
|
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
}
|
|
}
|
|
|
|
const rows: DiffRow[] = [];
|
|
let i = 0;
|
|
let j = 0;
|
|
let la = 1;
|
|
let lb = 1;
|
|
while (i < n && j < m) {
|
|
if (a[i] === b[j]) {
|
|
rows.push({ op: 'equal', text: a[i], leftNo: la++, rightNo: lb++ });
|
|
i++; j++;
|
|
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
rows.push({ op: 'del', text: a[i], leftNo: la++, rightNo: null });
|
|
i++;
|
|
} else {
|
|
rows.push({ op: 'add', text: b[j], leftNo: null, rightNo: lb++ });
|
|
j++;
|
|
}
|
|
}
|
|
while (i < n) rows.push({ op: 'del', text: a[i++], leftNo: la++, rightNo: null });
|
|
while (j < m) rows.push({ op: 'add', text: b[j++], leftNo: null, rightNo: lb++ });
|
|
return rows;
|
|
}
|
|
|
|
export interface DiffStats { added: number; removed: number; }
|
|
|
|
export function diffStats(rows: DiffRow[]): DiffStats {
|
|
let added = 0;
|
|
let removed = 0;
|
|
for (const r of rows) {
|
|
if (r.op === 'add') added++;
|
|
else if (r.op === 'del') removed++;
|
|
}
|
|
return { added, removed };
|
|
}
|
|
|
|
// sideBySide pairs rows into aligned [left, right] columns: a del with the next
|
|
// add becomes one changed row; otherwise each row occupies one side.
|
|
export interface SideRow {
|
|
left: DiffRow | null;
|
|
right: DiffRow | null;
|
|
}
|
|
|
|
export function sideBySide(rows: DiffRow[]): SideRow[] {
|
|
const out: SideRow[] = [];
|
|
for (let k = 0; k < rows.length; k++) {
|
|
const r = rows[k];
|
|
if (r.op === 'equal') {
|
|
out.push({ left: r, right: r });
|
|
} else if (r.op === 'del') {
|
|
// Pair consecutive del/add as a single changed line when possible.
|
|
const next = rows[k + 1];
|
|
if (next && next.op === 'add') {
|
|
out.push({ left: r, right: next });
|
|
k++;
|
|
} else {
|
|
out.push({ left: r, right: null });
|
|
}
|
|
} else {
|
|
out.push({ left: null, right: r });
|
|
}
|
|
}
|
|
return out;
|
|
}
|