| 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 |