Files
UI/packages/skin/ui/src/diffKit.ts
T

59 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*** 行级差异纯逻辑:切行 / 归一键 / 中间段 LCS 对齐;不碰 DOM,可被冒烟脚本直接跑 ***/
/*** 一条差异:same 原样、add 只在右边、del 只在左边 ***/
export type DiffOp = { type: 'same' | 'add' | 'del'; text: string };
/*** 比较用的一行:text 是显示原文,key 是比对键(忽略空白模式下把行内空白压成一个空格) ***/
export type DiffRow = { text: string; key: string };
// LCS 计算上限:中间段行数乘积超过这个值就退化为「整段替换」,避免大文件把主线程卡住
export const DP_LIMIT = 400_000;
/*** 作用:比对键归一——行内连续空白压成一个空格并去首尾空白;显示仍用原文,缩进不丢 ***/
export function normalizeLine(line: string): string {
return line.replace(/\s+/g, ' ').trim();
}
/*** 作用:切行并算比对键;末尾换行不产生一行空内容(否则每个文件都多一条假差异) ***/
export function toRows(text: string, ignore: boolean): DiffRow[] {
const lines = text.split(/\r?\n/);
if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop(); // 尾部换行忽略
return lines.map((line) => ({ text: line, key: ignore ? normalizeLine(line) : line }));
}
/*** 作用:中间段做 LCS 对齐,输出 del / add / same 序列;超限或一侧为空时退化为整段替换 ***/
export function alignMiddle(a: DiffRow[], b: DiffRow[]): DiffOp[] {
if (!a.length || !b.length) return [...a.map((r) => ({ type: 'del' as const, text: r.text })), ...b.map((r) => ({ type: 'add' as const, text: r.text }))];
if (a.length * b.length > DP_LIMIT) return [...a.map((r) => ({ type: 'del' as const, text: r.text })), ...b.map((r) => ({ type: 'add' as const, text: r.text }))]; // 超限:语义仍正确,只是不再逐行对齐
const dp: number[][] = Array.from({ length: a.length + 1 }, () => new Array<number>(b.length + 1).fill(0)); // dp[i][j]:a 前 i 行与 b 前 j 行的 LCS 长度
for (let i = a.length - 1; i >= 0; i -= 1) {
for (let j = b.length - 1; j >= 0; j -= 1) {
dp[i][j] = a[i].key === b[j].key ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
const ops: DiffOp[] = [];
let i = 0;
let j = 0;
while (i < a.length && j < b.length) {
if (a[i].key === b[j].key) { ops.push({ type: 'same', text: a[i].text }); i += 1; j += 1; continue; }
if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push({ type: 'del', text: a[i].text }); i += 1; continue; }
ops.push({ type: 'add', text: b[j].text }); j += 1;
}
for (; i < a.length; i += 1) ops.push({ type: 'del', text: a[i].text });
for (; j < b.length; j += 1) ops.push({ type: 'add', text: b[j].text });
return ops;
}
/*** 作用:逐行差异——剪公共前后缀后只对中间段做对齐,日常改动只占几行 ***/
export function diffLines(before: string, after: string, ignore: boolean): DiffOp[] {
const a = toRows(before, ignore);
const b = toRows(after, ignore);
let head = 0;
while (head < a.length && head < b.length && a[head].key === b[head].key) head += 1; // 公共前缀
let tail = 0;
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail].key === b[b.length - 1 - tail].key) tail += 1; // 公共后缀
return [
...a.slice(0, head).map((r) => ({ type: 'same' as const, text: r.text })),
...alignMiddle(a.slice(head, a.length - tail), b.slice(head, b.length - tail)),
...a.slice(a.length - tail).map((r) => ({ type: 'same' as const, text: r.text })),
];
}