返回 DeepSeek-Reasonix
diff.ts
根目录 / desktop / frontend / src / lib / diff.ts
1 export type DiffRow = {
2 type: "ctx" | "add" | "del";
3 text: string;
4 oldLine?: number;
5 newLine?: number;
6 };
7
8 // diffLines is a classic LCS line diff. Used by the diff seam to render edit-tool
9 // before/after; a real editor (Monaco/CodeMirror merge) would replace the
10 // rendering, but this keeps the algorithm in one place.
11 export function diffLines(a: string, b: string): DiffRow[] {
12 const x = a.split("\n");
13 const y = b.split("\n");
14 const n = x.length;
15 const m = y.length;
16 const dp: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0));
17 for (let i = n - 1; i >= 0; i--) {
18 for (let j = m - 1; j >= 0; j--) {
19 dp[i][j] = x[i] === y[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
20 }
21 }
22 const rows: DiffRow[] = [];
23 let i = 0;
24 let j = 0;
25 let oldLine = 1;
26 let newLine = 1;
27 while (i < n && j < m) {
28 if (x[i] === y[j]) {
29 rows.push({ type: "ctx", text: x[i], oldLine, newLine });
30 i++;
31 j++;
32 oldLine++;
33 newLine++;
34 } else if (dp[i + 1][j] >= dp[i][j + 1]) {
35 rows.push({ type: "del", text: x[i], oldLine });
36 i++;
37 oldLine++;
38 } else {
39 rows.push({ type: "add", text: y[j], newLine });
40 j++;
41 newLine++;
42 }
43 }
44 while (i < n) {
45 rows.push({ type: "del", text: x[i++], oldLine });
46 oldLine++;
47 }
48 while (j < m) {
49 rows.push({ type: "add", text: y[j++], newLine });
50 newLine++;
51 }
52 return rows;
53 }
54
55 const hunkHeader = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/;
56
57 // diffRowsFromUnifiedDiff renders an already-computed unified diff while keeping
58 // the real hunk line numbers. This is used when the backend previewed a writer
59 // tool against the whole file, so the UI does not have to re-diff tiny args
60 // snippets and accidentally restart line numbers at 1.
61 export function diffRowsFromUnifiedDiff(diff: string): DiffRow[] {
62 const rows: DiffRow[] = [];
63 let oldLine = 0;
64 let newLine = 0;
65 let inHunk = false;
66
67 const lines = diff.endsWith("\n") ? diff.slice(0, -1).split("\n") : diff.split("\n");
68 for (const line of lines) {
69 const header = hunkHeader.exec(line);
70 if (header) {
71 oldLine = Number(header[1]);
72 newLine = Number(header[2]);
73 inHunk = true;
74 continue;
75 }
76 if (!inHunk) continue;
77 if (line.startsWith("\\ No newline")) continue;
78
79 const marker = line[0];
80 const text = marker === " " || marker === "+" || marker === "-" ? line.slice(1) : line;
81 if (marker === "+") {
82 rows.push({ type: "add", text, newLine });
83 newLine++;
84 continue;
85 }
86 if (marker === "-") {
87 rows.push({ type: "del", text, oldLine });
88 oldLine++;
89 continue;
90 }
91 rows.push({ type: "ctx", text, oldLine, newLine });
92 oldLine++;
93 newLine++;
94 }
95
96 return rows;
97 }
98
99 // cleanGitDiff strips standard git diff headers (diff --git, index, ---, +++)
100 // and hunk headers (@@ -x,y +x,y @@) so the view focuses directly on the changed lines.
101 export function cleanGitDiff(diff: string): string {
102 const lines = diff.split("\n");
103 const cleaned: string[] = [];
104 let inHunk = false;
105
106 for (const line of lines) {
107 if (line.startsWith("@@ ")) {
108 inHunk = true;
109 // Skip the @@ line itself, optionally we could keep context if needed,
110 // but the user wants pure code changes.
111 continue;
112 }
113 if (inHunk) {
114 cleaned.push(line);
115 }
116 }
117
118 // If no hunks were found (unlikely for a valid diff), fallback to original logic
119 if (cleaned.length === 0) {
120 const match = diff.match(/^@@\s/m);
121 if (match && match.index !== undefined) {
122 return diff.slice(match.index).replace(/^@@.*$\n?/gm, "");
123 }
124 return diff;
125 }
126
127 return cleaned.join("\n");
128 }
129
129 lines TYPESCRIPT