diff --git a/apps/x/apps/renderer/src/components/code/cm.ts b/apps/x/apps/renderer/src/components/code/cm.ts index 7fe9d922..4b75c1b2 100644 --- a/apps/x/apps/renderer/src/components/code/cm.ts +++ b/apps/x/apps/renderer/src/components/code/cm.ts @@ -53,6 +53,18 @@ export function cmBaseExtensions(isDark: boolean): Extension[] { color: isDark ? '#6b7280' : '#9ca3af', }, '&.cm-focused': { outline: 'none' }, + // GitHub-style expander bar for folded unchanged regions (@codemirror/merge). + '.cm-collapsedLines': { + backgroundColor: isDark ? 'rgba(56, 139, 253, 0.15)' : 'rgba(9, 105, 218, 0.08)', + backgroundImage: 'none', + color: isDark ? '#79c0ff' : '#0969da', + padding: '4px 12px', + fontSize: '11px', + cursor: 'pointer', + }, + '.cm-collapsedLines:hover': { + backgroundColor: isDark ? 'rgba(56, 139, 253, 0.25)' : 'rgba(9, 105, 218, 0.15)', + }, }, { dark: isDark }, ), diff --git a/apps/x/apps/renderer/src/components/code/diff-viewer.tsx b/apps/x/apps/renderer/src/components/code/diff-viewer.tsx index 6d34e498..5b6c7b4d 100644 --- a/apps/x/apps/renderer/src/components/code/diff-viewer.tsx +++ b/apps/x/apps/renderer/src/components/code/diff-viewer.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import { MergeView, unifiedMergeView } from '@codemirror/merge' import { EditorView } from '@codemirror/view' -import { Columns2, Rows2, X } from 'lucide-react' +import { Columns2, FoldVertical, Rows2, UnfoldVertical, X } from 'lucide-react' import { useTheme } from '@/contexts/theme-context' import { Button } from '@/components/ui/button' import { cmBaseExtensions, cmLanguageFor } from './cm' @@ -22,6 +22,9 @@ export function DiffViewer({ const isDark = resolvedTheme === 'dark' const containerRef = useRef(null) const [mode, setMode] = useState<'split' | 'unified'>('split') + // GitHub-style: unchanged regions fold into "⋯ N lines" bars (each clickable + // to reveal); "Expand all" rebuilds the view with nothing collapsed. + const [collapseUnchanged, setCollapseUnchanged] = useState(true) const [diff, setDiff] = useState<{ oldText: string; newText: string; isBinary: boolean; tooLarge: boolean } | null>(null) const [error, setError] = useState(null) @@ -44,19 +47,27 @@ export function DiffViewer({ void cmLanguageFor(path).then((language) => { if (cancelled || !containerRef.current) return const extensions = [...cmBaseExtensions(isDark), ...(language ? [language] : [])] + // Same context margins GitHub uses: keep a few lines around each hunk, + // only fold stretches long enough to be worth hiding. + const collapse = collapseUnchanged ? { margin: 3, minSize: 6 } : undefined if (mode === 'split') { view = new MergeView({ a: { doc: diff.oldText, extensions }, b: { doc: diff.newText, extensions }, parent, gutter: true, + ...(collapse ? { collapseUnchanged: collapse } : {}), }) } else { view = new EditorView({ doc: diff.newText, extensions: [ ...extensions, - unifiedMergeView({ original: diff.oldText, mergeControls: false }), + unifiedMergeView({ + original: diff.oldText, + mergeControls: false, + ...(collapse ? { collapseUnchanged: collapse } : {}), + }), ], parent, }) @@ -67,12 +78,22 @@ export function DiffViewer({ cancelled = true view?.destroy() } - }, [diff, mode, isDark, path]) + }, [diff, mode, isDark, path, collapseUnchanged]) return (
{path} +