import { useMemo, useState } from 'react' import { cn } from '../../lib/utils' export interface FileDiff { path: string oldPath?: string additions: number deletions: number patch: string } interface DiffViewerProps { files: FileDiff[] } export function DiffViewer({ files }: DiffViewerProps) { if (!files.length) { return (
No changes in this diff.
) } return (
{files.map(file => ( ))}
) } function FileDiffBlock({ file }: { file: FileDiff }) { const [collapsed, setCollapsed] = useState(false) const lines = useMemo(() => parsePatch(file.patch), [file.patch]) return (
{/* File header */}
{file.additions > 0 && ( +{file.additions} )} {file.deletions > 0 && ( -{file.deletions} )}
{/* Diff lines */} {!collapsed && (
{lines.map((line, i) => ( ))}
)}
) } type LineType = 'added' | 'removed' | 'context' | 'hunk' interface ParsedLine { type: LineType oldNo?: number newNo?: number content: string } function parsePatch(patch: string): ParsedLine[] { const lines: ParsedLine[] = [] let oldNo = 0 let newNo = 0 for (const raw of patch.split('\n')) { if (raw.startsWith('@@')) { // @@ -oldStart,oldCount +newStart,newCount @@ const match = raw.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/) if (match) { oldNo = parseInt(match[1], 10) newNo = parseInt(match[2], 10) } lines.push({ type: 'hunk', content: raw }) continue } if (raw.startsWith('+') && !raw.startsWith('+++')) { lines.push({ type: 'added', newNo: newNo++, content: raw.slice(1) }) } else if (raw.startsWith('-') && !raw.startsWith('---')) { lines.push({ type: 'removed', oldNo: oldNo++, content: raw.slice(1) }) } else if (!raw.startsWith('\\') && !raw.startsWith('---') && !raw.startsWith('+++')) { lines.push({ type: 'context', oldNo: oldNo++, newNo: newNo++, content: raw }) } } return lines } function DiffLine({ line }: { line: ParsedLine }) { if (line.type === 'hunk') { return ( {line.content} ) } const bg = line.type === 'added' ? 'bg-[#E3FCEF]' : line.type === 'removed' ? 'bg-[var(--c-danger-tint)]' : '' const gutter = line.type === 'added' ? 'bg-[#ABF5D1] text-[#006644]' : line.type === 'removed' ? 'bg-[#FFBDAD] text-[var(--c-danger-dark)]' : 'bg-[var(--c-surface-muted)] text-[var(--c-muted)]' const prefix = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' ' return ( {line.type !== 'added' ? line.oldNo : ''} {line.type !== 'removed' ? line.newNo : ''} {prefix} {line.content} ) }