11'use client'
22
3+ import type { FileDiffOptions } from '@pierre/diffs'
34import type { ReactNode } from 'react'
45import type { GitDiff } from '../../../index'
6+ import { parsePatchFiles } from '@pierre/diffs'
7+ import { FileDiff } from '@pierre/diffs/react'
8+ import { useMemo } from 'react'
59import { cn } from '../../lib/utils'
10+ import { useColorScheme } from '../theme'
611import { Badge } from '../ui/badge'
712import { IconButton } from '../ui/button'
813import { Icon } from '../ui/icon'
@@ -21,39 +26,53 @@ export interface DiffPanelViewProps {
2126 patchSlot ?: ReactNode
2227}
2328
24- function patchLineClass ( line : string ) : string {
25- if ( line . startsWith ( '@@' ) )
26- return 'color-active'
27- if ( line . startsWith ( '+' ) && ! line . startsWith ( '+++' ) )
28- return 'text-success'
29- if ( line . startsWith ( '-' ) && ! line . startsWith ( '---' ) )
30- return 'text-error'
31- if ( line . startsWith ( 'diff ' ) || line . startsWith ( 'index ' ) || line . startsWith ( '+++' ) || line . startsWith ( '---' ) )
32- return 'color-muted font-semibold'
33- return 'color-base'
29+ // Shiki themes for the diff renderer, chosen to sit alongside the @antfu/design
30+ // surfaces; @pierre/diffs picks light vs. dark from `themeType`.
31+ const DIFF_THEME = { light : 'vitesse-light' , dark : 'vitesse-dark' } as const
32+
33+ /** Split a unified/git patch into its per-file diffs, tolerant of truncation. */
34+ function parsePatch ( patch : string ) {
35+ try {
36+ return parsePatchFiles ( patch ) . flatMap ( p => p . files )
37+ }
38+ catch {
39+ return [ ]
40+ }
3441}
3542
3643/**
37- * Pure renderer for a unified patch. Set `scroll={false}` to render inline
38- * (no inner scroll area) when the patch already sits in a scrolling parent.
44+ * Renders a unified git patch with `@pierre/diffs` (diffs.com) — Shiki syntax
45+ * highlighting, per-file headers, and a theme synced to the app. Set
46+ * `scroll={false}` to render inline (no inner scroll area) when the patch
47+ * already sits in a scrolling parent.
3948 */
4049export function DiffPatchView ( { patch, loading, truncated, scroll = true } : { patch : string | null , loading : boolean , truncated : boolean , scroll ?: boolean } ) {
50+ const scheme = useColorScheme ( )
51+ const files = useMemo ( ( ) => ( patch ? parsePatch ( patch ) : [ ] ) , [ patch ] )
52+ const options = useMemo < FileDiffOptions < undefined > > ( ( ) => ( {
53+ theme : DIFF_THEME ,
54+ themeType : scheme ,
55+ diffStyle : 'unified' ,
56+ diffIndicators : 'classic' ,
57+ } ) , [ scheme ] )
58+
4159 if ( loading )
4260 return < Skeleton className = "h-40 w-full" />
43- if ( ! patch )
61+ if ( ! patch || files . length === 0 )
4462 return < p className = "color-muted p-3 text-sm" > No textual diff available (binary or unchanged).</ p >
63+
4564 const body = (
4665 < >
47- < pre className = "font-mono text-xs leading-relaxed " >
48- { patch . split ( '\n' ) . map ( ( line , i ) => (
49- < div key = { i } className = { cn ( 'px-3 whitespace-pre' , patchLineClass ( line ) ) } > { line || ' ' } </ div >
66+ < div className = "flex flex-col text-sm [&>*+*]:border-t [&>*+*]:border-base " >
67+ { files . map ( ( file , i ) => (
68+ < FileDiff key = { file . name || i } fileDiff = { file } options = { options } disableWorkerPool / >
5069 ) ) }
51- </ pre >
70+ </ div >
5271 { truncated && < p className = "text-warning px-3 py-1 text-xs" > Patch truncated.</ p > }
5372 </ >
5473 )
5574 if ( ! scroll )
56- return < div className = "overflow-x-auto py-1" > { body } </ div >
75+ return < div > { body } </ div >
5776 return < ScrollArea className = "h-72 w-full" > { body } </ ScrollArea >
5877}
5978
@@ -154,7 +173,6 @@ export function DiffPanelView(props: DiffPanelViewProps) {
154173
155174 { selected && (
156175 < div className = "overflow-hidden rounded-md border" >
157- < div className = "bg-secondary border-b px-3 py-1 font-mono text-xs" > { selected } </ div >
158176 { patchSlot }
159177 </ div >
160178 ) }
0 commit comments