(lines: string[], startingLineNumber: number, width: number, dim: boolean, overrideTheme?: ThemeName)
| 347 | }); |
| 348 | } |
| 349 | function formatDiff(lines: string[], startingLineNumber: number, width: number, dim: boolean, overrideTheme?: ThemeName): React.ReactNode[] { |
| 350 | // Ensure width is at least 1 to prevent rendering issues with very narrow terminals |
| 351 | const safeWidth = Math.max(1, Math.floor(width)); |
| 352 | |
| 353 | // Step 1: Transform lines to line objects with type information |
| 354 | const lineObjects = transformLinesToObjects(lines); |
| 355 | |
| 356 | // Step 2: Group adjacent add/remove lines for word-level diffing |
| 357 | const processedLines = processAdjacentLines(lineObjects); |
| 358 | |
| 359 | // Step 3: Number the diff lines |
| 360 | const ls = numberDiffLines(processedLines, startingLineNumber); |
| 361 | |
| 362 | // Find max line number width for alignment |
| 363 | const maxLineNumber = Math.max(...ls.map(({ |
| 364 | i |
| 365 | }) => i), 0); |
| 366 | const maxWidth = Math.max(maxLineNumber.toString().length + 1, 0); |
| 367 | |
| 368 | // Step 4: Render formatting |
| 369 | return ls.flatMap((item): React.ReactNode[] => { |
| 370 | const { |
| 371 | type, |
| 372 | code, |
| 373 | i, |
| 374 | wordDiff, |
| 375 | matchedLine |
| 376 | } = item; |
| 377 | |
| 378 | // Handle word-level diffing for add/remove pairs |
| 379 | if (wordDiff && matchedLine) { |
| 380 | const wordDiffElements = generateWordDiffElements(item, safeWidth, maxWidth, dim, overrideTheme); |
| 381 | |
| 382 | // word-diff might refuse (e.g. due to lines being substantially different) in which |
| 383 | // case we'll fall through to normal renderin gbelow |
| 384 | if (wordDiffElements !== null) { |
| 385 | return wordDiffElements; |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | // Standard rendering for lines without word diffing or as fallback |
| 390 | // Calculate available width accounting for line number + space + diff prefix |
| 391 | const diffPrefixWidth = 2; // " " for unchanged, "+ " or "- " for changes |
| 392 | const availableContentWidth = Math.max(1, safeWidth - maxWidth - 1 - diffPrefixWidth); // -1 for space after line number |
| 393 | const wrappedText = wrapText(code, availableContentWidth, 'wrap'); |
| 394 | const wrappedLines = wrappedText.split('\n'); |
| 395 | return wrappedLines.map((line, lineIndex) => { |
| 396 | const key = `${type}-${i}-${lineIndex}`; |
| 397 | const lineNum = lineIndex === 0 ? i : undefined; |
| 398 | const lineNumStr = (lineNum !== undefined ? lineNum.toString().padStart(maxWidth) : ' '.repeat(maxWidth)) + ' '; |
| 399 | const sigil = type === 'add' ? '+' : type === 'remove' ? '-' : ' '; |
| 400 | // Calculate padding to fill the entire terminal width |
| 401 | const contentWidth = lineNumStr.length + 1 + stringWidth(line); // lineNum + sigil + code |
| 402 | const padding = Math.max(0, safeWidth - contentWidth); |
| 403 | const bgColor = type === 'add' ? dim ? 'diffAddedDimmed' : 'diffAdded' : type === 'remove' ? dim ? 'diffRemovedDimmed' : 'diffRemoved' : undefined; |
| 404 | |
| 405 | // Gutter (line number + sigil) is wrapped in <NoSelect> so fullscreen |
| 406 | // text selection yields clean code. bgColor carries across both boxes |
no test coverage detected