Skip to content

Commit 2070066

Browse files
committed
feat(git): render diffs with @pierre/diffs (diffs.com)
Replace the git plugin's line-prefix `<pre>` patch renderer with the Shiki-based diffs.com library, giving syntax-highlighted, per-file diffs in both the working-tree/staged diff panel and the multi-file commit details view. The diff theme tracks the app's light/dark toggle.
1 parent 48aa791 commit 2070066

5 files changed

Lines changed: 132 additions & 19 deletions

File tree

plugins/git/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
},
5353
"devDependencies": {
5454
"@antfu/design": "catalog:frontend",
55+
"@pierre/diffs": "catalog:frontend",
5556
"@radix-ui/react-scroll-area": "catalog:frontend",
5657
"@radix-ui/react-separator": "catalog:frontend",
5758
"@radix-ui/react-slot": "catalog:frontend",

plugins/git/src/client/components/theme.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,24 @@ export function useTheme() {
4949

5050
return { theme, toggle }
5151
}
52+
53+
/**
54+
* The color scheme currently applied to the document, tracked by observing the
55+
* `.dark` class on `<html>`. Unlike {@link useTheme}, this reacts to toggles
56+
* made anywhere in the app (or by Storybook), so Shiki-themed surfaces like the
57+
* diff viewer stay in step with the rest of the UI regardless of who flipped it.
58+
*/
59+
export function useColorScheme(): Theme {
60+
const [scheme, setScheme] = useState<Theme>('dark')
61+
62+
useEffect(() => {
63+
const root = document.documentElement
64+
const read = () => setScheme(root.classList.contains('dark') ? 'dark' : 'light')
65+
read()
66+
const observer = new MutationObserver(read)
67+
observer.observe(root, { attributes: true, attributeFilter: ['class'] })
68+
return () => observer.disconnect()
69+
}, [])
70+
71+
return scheme
72+
}

plugins/git/src/client/components/views/diff-panel-view.tsx

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
'use client'
22

3+
import type { FileDiffOptions } from '@pierre/diffs'
34
import type { ReactNode } from 'react'
45
import type { GitDiff } from '../../../index'
6+
import { parsePatchFiles } from '@pierre/diffs'
7+
import { FileDiff } from '@pierre/diffs/react'
8+
import { useMemo } from 'react'
59
import { cn } from '../../lib/utils'
10+
import { useColorScheme } from '../theme'
611
import { Badge } from '../ui/badge'
712
import { IconButton } from '../ui/button'
813
import { 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
*/
4049
export 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
)}

pnpm-lock.yaml

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ strictPeerDependencies: false
1919
trustPolicy: no-downgrade
2020
trustPolicyExclude:
2121
- tinyexec@1.2.2
22+
- '@pierre/theme@1.1.0'
2223
packages:
2324
- packages/*
2425
- plugins/*
@@ -77,6 +78,7 @@ catalogs:
7778
frontend:
7879
'@antfu/design': ^0.2.1
7980
'@iconify-json/ph': ^1.2.2
81+
'@pierre/diffs': ^1.2.12
8082
'@radix-ui/react-scroll-area': ^1.2.14
8183
'@radix-ui/react-separator': ^1.1.11
8284
'@radix-ui/react-slot': ^1.3.0

0 commit comments

Comments
 (0)