| 238 | |
| 239 | /** Display-oriented diff with line numbers and bounded context. */ |
| 240 | export function generateDiffString( |
| 241 | oldContent: string, |
| 242 | newContent: string, |
| 243 | contextLines = 4, |
| 244 | ): EditDiffResult { |
| 245 | const parts = Diff.diffLines(oldContent, newContent); |
| 246 | const output: string[] = []; |
| 247 | |
| 248 | const oldLines = oldContent.split("\n"); |
| 249 | const newLines = newContent.split("\n"); |
| 250 | const maxLineNum = Math.max(oldLines.length, newLines.length); |
| 251 | const w = String(maxLineNum).length; |
| 252 | |
| 253 | let oldLineNum = 1; |
| 254 | let newLineNum = 1; |
| 255 | let lastWasChange = false; |
| 256 | let firstChangedLine: number | undefined; |
| 257 | |
| 258 | for (let i = 0; i < parts.length; i++) { |
| 259 | const part = parts[i]; |
| 260 | const raw = part.value.split("\n"); |
| 261 | if (raw[raw.length - 1] === "") raw.pop(); |
| 262 | |
| 263 | if (part.added || part.removed) { |
| 264 | if (firstChangedLine === undefined) firstChangedLine = newLineNum; |
| 265 | for (const line of raw) { |
| 266 | if (part.added) { |
| 267 | output.push(`+${String(newLineNum).padStart(w, " ")} ${line}`); |
| 268 | newLineNum++; |
| 269 | } else { |
| 270 | output.push(`-${String(oldLineNum).padStart(w, " ")} ${line}`); |
| 271 | oldLineNum++; |
| 272 | } |
| 273 | } |
| 274 | lastWasChange = true; |
| 275 | } else { |
| 276 | const nextIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed); |
| 277 | const leading = lastWasChange; |
| 278 | const trailing = nextIsChange; |
| 279 | |
| 280 | const emit = (line: string) => { |
| 281 | output.push(` ${String(oldLineNum).padStart(w, " ")} ${line}`); |
| 282 | oldLineNum++; |
| 283 | newLineNum++; |
| 284 | }; |
| 285 | |
| 286 | if (leading && trailing) { |
| 287 | if (raw.length <= contextLines * 2) { |
| 288 | raw.forEach(emit); |
| 289 | } else { |
| 290 | raw.slice(0, contextLines).forEach(emit); |
| 291 | const skipped = raw.length - contextLines * 2; |
| 292 | output.push(` ${"".padStart(w, " ")} ...`); |
| 293 | oldLineNum += skipped; |
| 294 | newLineNum += skipped; |
| 295 | raw.slice(raw.length - contextLines).forEach(emit); |
| 296 | } |
| 297 | } else if (leading) { |