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}
|
)
}