import { ClientOnly } from "@tanstack/react-router"; import { parsePatchFiles } from "@pierre/diffs"; import type { DiffLineAnnotation, FileDiffMetadata, SelectedLineRange } from "@pierre/diffs"; import { FileDiff } from "@pierre/diffs/react"; import { useState } from "react"; import { Button } from "./ui/Button"; import { EmptyState } from "./ui/Primitives"; import { ImageDiff, isImagePath } from "./rich/ImageDiff"; import type { ImageSources } from "./rich/ImageDiff"; import type { DiffResult } from "~/lib/types"; export type DiffSide = "LEFT" | "RIGHT"; export type LineAnchor = { path: string; line: number; side: DiffSide; data: T }; /** First line of a multi-line selection; the anchor line is its last line. */ export type RangeStart = { line: number; side: DiffSide }; type LineAction = (path: string, line: number, side: DiffSide, start?: RangeStart) => void; function selectionAction(path: string, range: SelectedLineRange, onLineAction: LineAction) { const startSide: DiffSide = range.side === "deletions" ? "LEFT" : "RIGHT"; const endSide: DiffSide = (range.endSide ?? range.side) === "deletions" ? "LEFT" : "RIGHT"; if (startSide !== endSide || range.start === range.end) { onLineAction(path, range.end, endSide); return; } const [first, last] = range.start < range.end ? [range.start, range.end] : [range.end, range.start]; onLineAction(path, last, endSide, { line: first, side: startSide }); } type Props = { diff: DiffResult; anchors?: LineAnchor[]; renderAnchor?: (anchor: LineAnchor) => React.ReactNode; onLineAction?: LineAction; images?: ImageSources; }; const DIFF_STYLES = ["split", "unified"] as const; function toAnnotationSide(side: DiffSide): "additions" | "deletions" { return side === "RIGHT" ? "additions" : "deletions"; } function FileBlock({ fileDiff, diffStyle, anchors, renderAnchor, onLineAction, }: { fileDiff: FileDiffMetadata; diffStyle: (typeof DIFF_STYLES)[number]; anchors: LineAnchor[]; renderAnchor?: (anchor: LineAnchor) => React.ReactNode; onLineAction?: LineAction; }) { const annotations: DiffLineAnnotation>[] = anchors.map((anchor) => ({ side: toAnnotationSide(anchor.side), lineNumber: anchor.line, metadata: anchor, })); return (
> fileDiff={fileDiff} lineAnnotations={annotations} renderAnnotation={(annotation) => (renderAnchor ? renderAnchor(annotation.metadata) : null)} options={{ theme: "pierre-dark", diffStyle, diffIndicators: "bars", hunkSeparators: "line-info", overflow: "scroll", enableGutterUtility: Boolean(onLineAction), onGutterUtilityClick: (range) => { if (onLineAction) selectionAction(fileDiff.name, range, onLineAction); }, }} />
); } export function DiffView({ diff, anchors = [], renderAnchor, onLineAction, images }: Props) { const [diffStyle, setDiffStyle] = useState<(typeof DIFF_STYLES)[number]>("split"); if (!diff.patch.trim()) return ; const imageFiles = images ? diff.files.filter((file) => file.binary && isImagePath(file.path)) : []; const imagePaths = new Set(imageFiles.map((file) => file.path)); const files = parsePatchFiles(diff.patch) .flatMap((patch) => patch.files) .filter((fileDiff) => !imagePaths.has(fileDiff.name)); const truncatedFiles = diff.files.filter((file) => file.truncated); return (
{diff.files.length} file{diff.files.length === 1 ? "" : "s"} changed +{diff.additions} −{diff.deletions}
{truncatedFiles.length > 0 && (

{truncatedFiles.length} large file(s) are not rendered:{" "} {truncatedFiles .slice(0, 5) .map((file) => file.path) .join(", ")}

)} {images && imageFiles.map((file) => ( ))} Rendering diff…

}> {files.map((fileDiff) => ( anchor.path === fileDiff.name)} renderAnchor={renderAnchor} onLineAction={onLineAction} /> ))}
); }