// 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; }