"use client"; import { useState, useMemo } from "react"; type DiffCardProps = { /** Raw unified diff text (contents of a ```diff block) */ diff: string; }; type DiffFile = { oldPath: string; newPath: string; hunks: DiffHunk[]; additions: number; deletions: number; }; type DiffHunk = { header: string; lines: DiffLine[]; }; type DiffLine = { type: "addition" | "deletion" | "context" | "header"; content: string; oldLine?: number; newLine?: number; }; /** Parse unified diff text into structured file sections. */ function parseDiff(raw: string): DiffFile[] { const files: DiffFile[] = []; const lines = raw.split("\n"); let current: DiffFile | null = null; let currentHunk: DiffHunk | null = null; let oldLine = 0; let newLine = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // File header: --- a/path or --- /dev/null if (line.startsWith("--- ")) { const nextLine = lines[i + 1]; if (nextLine?.startsWith("+++ ")) { const oldPath = line.replace(/^--- (a\/)?/, "").trim(); const newPath = nextLine.replace(/^\+\+\+ (b\/)?/, "").trim(); current = { oldPath, newPath, hunks: [], additions: 0, deletions: 0 }; files.push(current); i++; // skip +++ line continue; } } // Hunk header: @@ -old,count +new,count @@ const hunkMatch = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@(.*)/); if (hunkMatch) { oldLine = parseInt(hunkMatch[1], 10); newLine = parseInt(hunkMatch[2], 10); currentHunk = { header: line, lines: [{ type: "header", content: line }], }; if (current) { current.hunks.push(currentHunk); } else { // Diff without file headers -- create an implicit file current = { oldPath: "", newPath: "", hunks: [currentHunk], additions: 0, deletions: 0 }; files.push(current); } continue; } if (!currentHunk || !current) {continue;} if (line.startsWith("+")) { currentHunk.lines.push({ type: "addition", content: line.slice(1), newLine }); current.additions++; newLine++; } else if (line.startsWith("-")) { currentHunk.lines.push({ type: "deletion", content: line.slice(1), oldLine }); current.deletions++; oldLine++; } else if (line.startsWith(" ") || line === "") { currentHunk.lines.push({ type: "context", content: line.slice(1) || "", oldLine, newLine }); oldLine++; newLine++; } } // If no structured files were found, treat the whole thing as one block if (files.length === 0 && raw.trim()) { const fallbackLines = raw.split("\n").map((l): DiffLine => { if (l.startsWith("+")) {return { type: "addition", content: l.slice(1) };} if (l.startsWith("-")) {return { type: "deletion", content: l.slice(1) };} return { type: "context", content: l }; }); files.push({ oldPath: "", newPath: "", hunks: [{ header: "", lines: fallbackLines }], additions: fallbackLines.filter((l) => l.type === "addition").length, deletions: fallbackLines.filter((l) => l.type === "deletion").length, }); } return files; } function displayPath(file: DiffFile): string { if (file.newPath && file.newPath !== "/dev/null") {return file.newPath;} if (file.oldPath && file.oldPath !== "/dev/null") {return file.oldPath;} return "diff"; } /* ─── Icons ─── */ function FileIcon() { return ( ); } function ChevronIcon({ expanded }: { expanded: boolean }) { return ( ); } /* ─── Single file diff ─── */ function DiffFileCard({ file }: { file: DiffFile }) { const [expanded, setExpanded] = useState(true); const path = displayPath(file); return (
{/* File header */} {/* Diff lines */} {expanded && (
{file.hunks.map((hunk, hi) => hunk.lines.map((line, li) => { if (line.type === "header") { return ( ); } const bgColor = line.type === "addition" ? "var(--diff-add-bg)" : line.type === "deletion" ? "var(--diff-del-bg)" : "transparent"; const textColor = line.type === "addition" ? "var(--diff-add-text)" : line.type === "deletion" ? "var(--diff-del-text)" : "var(--color-text)"; const prefix = line.type === "addition" ? "+" : line.type === "deletion" ? "-" : " "; return ( {/* Old line number */} {/* New line number */} {/* Content */} ); }), )}
{line.content}
{line.type !== "addition" ? line.oldLine : ""} {line.type !== "deletion" ? line.newLine : ""} {prefix} {line.content}
)}
); } /* ─── Main DiffCard ─── */ export function DiffCard({ diff }: DiffCardProps) { const files = useMemo(() => parseDiff(diff), [diff]); return (
{files.map((file, i) => ( ))}
); }