/** * DiffRenderer Component * * Renders unified diff with syntax highlighting. * Shows filename, +N/-M stats, and colored diff lines. */ import { useState } from 'react'; import { FileEdit, ChevronDown, ChevronRight, Copy, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { DiffDisplayData } from '@dexto/core'; interface DiffRendererProps { /** Diff display data from tool result */ data: DiffDisplayData; /** Maximum lines before truncation (default: 50) */ maxLines?: number; /** Whether to start expanded (default: false) */ defaultExpanded?: boolean; } // ============================================================================= // Diff Parsing (ported from CLI) // ============================================================================= interface ParsedHunk { oldStart: number; newStart: number; lines: ParsedLine[]; } interface ParsedLine { type: 'context' | 'addition' | 'deletion'; content: string; lineNum: number; } /** * Parse unified diff into structured hunks. */ function parseUnifiedDiff(unified: string): ParsedHunk[] { const lines = unified.split('\n'); const hunks: ParsedHunk[] = []; let currentHunk: ParsedHunk | null = null; let oldLine = 0; let newLine = 0; for (const line of lines) { if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('Index:')) { continue; } const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/); if (hunkMatch) { if (currentHunk) { hunks.push(currentHunk); } oldLine = parseInt(hunkMatch[1]!, 10); newLine = parseInt(hunkMatch[3]!, 10); currentHunk = { oldStart: oldLine, newStart: newLine, lines: [], }; continue; } if (!currentHunk) continue; if (line.startsWith('+')) { currentHunk.lines.push({ type: 'addition', content: line.slice(1), lineNum: newLine++, }); } else if (line.startsWith('-')) { currentHunk.lines.push({ type: 'deletion', content: line.slice(1), lineNum: oldLine++, }); } else if (line.startsWith(' ') || line === '') { currentHunk.lines.push({ type: 'context', content: line.startsWith(' ') ? line.slice(1) : line, lineNum: newLine, }); oldLine++; newLine++; } } if (currentHunk) { hunks.push(currentHunk); } return hunks; } /** * Get line number width for consistent alignment. */ function getLineNumWidth(maxLineNum: number): number { return Math.max(3, String(maxLineNum).length); } /** * Format line number with padding. */ function formatLineNum(num: number, width: number): string { return String(num).padStart(width, ' '); } // ============================================================================= // Line Components // ============================================================================= interface DiffLineProps { type: 'context' | 'addition' | 'deletion'; lineNum: number; lineNumWidth: number; content: string; } /** * Render a single diff line with gutter and content. */ function DiffLine({ type, lineNum, lineNumWidth, content }: DiffLineProps) { const lineNumStr = formatLineNum(lineNum, lineNumWidth); const getStyles = () => { switch (type) { case 'deletion': return { bg: 'bg-red-100/50 dark:bg-red-900/20', text: 'text-red-800 dark:text-red-300', symbol: '-', symbolColor: 'text-red-600 dark:text-red-400', }; case 'addition': return { bg: 'bg-green-100/50 dark:bg-green-900/20', text: 'text-green-800 dark:text-green-300', symbol: '+', symbolColor: 'text-green-600 dark:text-green-400', }; default: return { bg: '', text: 'text-foreground/60', symbol: ' ', symbolColor: 'text-transparent', }; } }; const styles = getStyles(); return (
{content || ' '}