11import { parsePatchFiles } from "@pierre/diffs" ;
22import { FileDiff , type FileDiffMetadata , Virtualizer } from "@pierre/diffs/react" ;
33import { useQuery } from "@tanstack/react-query" ;
4- import { Columns2Icon , Rows3Icon , TextWrapIcon , XIcon } from "lucide-react" ;
4+ import {
5+ CheckIcon ,
6+ ChevronRightIcon ,
7+ Columns2Icon ,
8+ Rows3Icon ,
9+ TextWrapIcon ,
10+ XIcon ,
11+ } from "lucide-react" ;
512import { startTransition , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
613
714import { openInPreferredEditor } from "../editorPreferences" ;
@@ -57,10 +64,7 @@ const DIFF_PANEL_UNSAFE_CSS = `
5764}
5865
5966[data-file-info] {
60- background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important;
61- border-block-color: var(--border) !important;
62- color: var(--foreground) !important;
63- padding-right: 5.75rem !important;
67+ display: none !important;
6468}
6569
6670[data-diffs-header] {
@@ -172,6 +176,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
172176 const [ diffWordWrap , setDiffWordWrap ] = useState ( false ) ;
173177 const [ selectedCategory , setSelectedCategory ] = useState < FileDiffCategory > ( "all" ) ;
174178 const [ acceptedFileKeys , setAcceptedFileKeys ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
179+ const [ collapsedFileKeys , setCollapsedFileKeys ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
175180 const patchViewportRef = useRef < HTMLDivElement > ( null ) ;
176181 const previousDiffOpenRef = useRef ( false ) ;
177182
@@ -285,17 +290,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
285290 deleted : 0 ,
286291 renamed : 0 ,
287292 } ;
288- for ( const fileDiff of remainingFiles ) {
293+ for ( const fileDiff of renderableFiles ) {
289294 const category = categorizeFileDiff ( fileDiff ) ;
290295 counts [ category ] ++ ;
291296 }
292- return { all : remainingFiles . length , ...counts } ;
293- } , [ remainingFiles ] ) ;
297+ return { all : renderableFiles . length , ...counts } ;
298+ } , [ renderableFiles ] ) ;
294299
295300 const filteredFiles = useMemo ( ( ) => {
296- if ( selectedCategory === "all" ) return remainingFiles ;
297- return remainingFiles . filter ( ( fileDiff ) => categorizeFileDiff ( fileDiff ) === selectedCategory ) ;
298- } , [ remainingFiles , selectedCategory ] ) ;
301+ if ( selectedCategory === "all" ) return renderableFiles ;
302+ return renderableFiles . filter ( ( fileDiff ) => categorizeFileDiff ( fileDiff ) === selectedCategory ) ;
303+ } , [ renderableFiles , selectedCategory ] ) ;
299304
300305 useEffect ( ( ) => {
301306 if ( diffOpen && ! previousDiffOpenRef . current ) {
@@ -307,16 +312,31 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
307312
308313 useEffect ( ( ) => {
309314 setAcceptedFileKeys ( new Set ( ) ) ;
315+ setCollapsedFileKeys ( new Set ( ) ) ;
310316 } , [ selectedPatch ] ) ;
311317
312318 useEffect ( ( ) => {
313319 if ( ! selectedFilePath || ! patchViewportRef . current ) {
314320 return ;
315321 }
316- const target = Array . from (
317- patchViewportRef . current . querySelectorAll < HTMLElement > ( "[data-diff-file-path]" ) ,
318- ) . find ( ( element ) => element . dataset . diffFilePath === selectedFilePath ) ;
319- target ?. scrollIntoView ( { block : "nearest" } ) ;
322+ const selectedFile = renderableFiles . find (
323+ ( f ) => resolveFileDiffPath ( f ) === selectedFilePath ,
324+ ) ;
325+ if ( selectedFile ) {
326+ const key = buildFileDiffRenderKey ( selectedFile ) ;
327+ setCollapsedFileKeys ( ( current ) => {
328+ if ( ! current . has ( key ) ) return current ;
329+ const next = new Set ( current ) ;
330+ next . delete ( key ) ;
331+ return next ;
332+ } ) ;
333+ }
334+ requestAnimationFrame ( ( ) => {
335+ const target = Array . from (
336+ patchViewportRef . current ?. querySelectorAll < HTMLElement > ( "[data-diff-file-path]" ) ?? [ ] ,
337+ ) . find ( ( element ) => element . dataset . diffFilePath === selectedFilePath ) ;
338+ target ?. scrollIntoView ( { block : "nearest" } ) ;
339+ } ) ;
320340 } , [ selectedFilePath , renderableFiles ] ) ;
321341
322342 const openDiffFileInEditor = useCallback (
@@ -333,6 +353,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
333353
334354 const acceptFile = useCallback ( ( fileDiff : FileDiffMetadata ) => {
335355 const fileKey = buildAcceptedDiffFileKey ( fileDiff ) ;
356+ const renderKey = buildFileDiffRenderKey ( fileDiff ) ;
336357 startTransition ( ( ) => {
337358 setAcceptedFileKeys ( ( current ) => {
338359 if ( current . has ( fileKey ) ) {
@@ -342,6 +363,12 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
342363 next . add ( fileKey ) ;
343364 return next ;
344365 } ) ;
366+ setCollapsedFileKeys ( ( current ) => {
367+ if ( current . has ( renderKey ) ) return current ;
368+ const next = new Set ( current ) ;
369+ next . add ( renderKey ) ;
370+ return next ;
371+ } ) ;
345372 } ) ;
346373 } , [ ] ) ;
347374
@@ -357,11 +384,29 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
357384 }
358385 return next ;
359386 } ) ;
387+ setCollapsedFileKeys ( ( current ) => {
388+ const next = new Set ( current ) ;
389+ for ( const fileDiff of remainingFiles ) {
390+ next . add ( buildFileDiffRenderKey ( fileDiff ) ) ;
391+ }
392+ return next ;
393+ } ) ;
360394 } ) ;
361395 } , [ remainingFiles ] ) ;
362396
363- const allFilesAccepted = renderableFiles . length > 0 && remainingFiles . length === 0 ;
364- const noFilesInSelectedCategory = ! allFilesAccepted && filteredFiles . length === 0 ;
397+ const toggleFileCollapse = useCallback ( ( fileKey : string ) => {
398+ setCollapsedFileKeys ( ( current ) => {
399+ const next = new Set ( current ) ;
400+ if ( next . has ( fileKey ) ) {
401+ next . delete ( fileKey ) ;
402+ } else {
403+ next . add ( fileKey ) ;
404+ }
405+ return next ;
406+ } ) ;
407+ } , [ ] ) ;
408+
409+ const noFilesInSelectedCategory = filteredFiles . length === 0 ;
365410
366411 const headerRow = (
367412 < >
@@ -485,13 +530,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
485530 </ div >
486531 )
487532 ) : renderablePatch . kind === "files" ? (
488- allFilesAccepted || noFilesInSelectedCategory ? (
533+ noFilesInSelectedCategory ? (
489534 < div className = "flex h-full items-center justify-center px-3 py-2 text-xs text-muted-foreground/70" >
490- < p >
491- { allFilesAccepted
492- ? "All file changes accepted."
493- : `No remaining ${ CATEGORY_LABELS [ selectedCategory ] . toLowerCase ( ) } changes.` }
494- </ p >
535+ < p > { `No ${ CATEGORY_LABELS [ selectedCategory ] . toLowerCase ( ) } changes.` } </ p >
495536 </ div >
496537 ) : (
497538 < Virtualizer
@@ -505,44 +546,113 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
505546 const filePath = resolveFileDiffPath ( fileDiff ) ;
506547 const fileKey = buildFileDiffRenderKey ( fileDiff ) ;
507548 const themedFileKey = `${ fileKey } :${ resolvedTheme } ` ;
549+ const isAccepted = acceptedFileKeys . has (
550+ buildAcceptedDiffFileKey ( fileDiff ) ,
551+ ) ;
552+ const isCollapsed = collapsedFileKeys . has ( fileKey ) ;
553+ const changeType = categorizeFileDiff ( fileDiff ) ;
508554 return (
509555 < div
510556 key = { themedFileKey }
511557 data-diff-file-path = { filePath }
512- className = "diff-render-file relative mb-2 rounded-md first:mt-2 last:mb-0"
513- onClickCapture = { ( event ) => {
514- const nativeEvent = event . nativeEvent as MouseEvent ;
515- const composedPath = nativeEvent . composedPath ?.( ) ?? [ ] ;
516- const clickedHeader = composedPath . some ( ( node ) => {
517- if ( ! ( node instanceof Element ) ) return false ;
518- return node . hasAttribute ( "data-title" ) ;
519- } ) ;
520- if ( ! clickedHeader ) return ;
521- openDiffFileInEditor ( filePath ) ;
522- } }
558+ className = "diff-render-file mb-2 first:mt-2 last:mb-0"
523559 >
524- < div className = "pointer-events-none absolute right-2 top-2 z-10" >
525- < Button
560+ < div
561+ className = { cn (
562+ "overflow-hidden rounded-md border transition-colors duration-150" ,
563+ isAccepted ? "border-border/40" : "border-border/70" ,
564+ ) }
565+ >
566+ < button
526567 type = "button"
527- size = "xs"
528- variant = "secondary"
529- className = "pointer-events-auto"
530- onClick = { ( ) => acceptFile ( fileDiff ) }
568+ onClick = { ( ) => toggleFileCollapse ( fileKey ) }
569+ className = { cn (
570+ "flex w-full items-center gap-2 px-3 py-2 text-left" ,
571+ "bg-[color-mix(in_srgb,var(--card)_94%,var(--foreground))]" ,
572+ "hover:bg-[color-mix(in_srgb,var(--card)_90%,var(--foreground))]" ,
573+ "transition-colors duration-150" ,
574+ ! isCollapsed && "border-b border-border/50" ,
575+ isAccepted && "opacity-60 hover:opacity-80" ,
576+ ) }
531577 >
532- Accept
533- </ Button >
578+ < ChevronRightIcon
579+ className = { cn (
580+ "size-3.5 shrink-0 text-muted-foreground/70 transition-transform duration-200" ,
581+ ! isCollapsed && "rotate-90" ,
582+ ) }
583+ />
584+ < span
585+ role = "link"
586+ tabIndex = { 0 }
587+ className = "min-w-0 flex-1 truncate font-mono text-[11px] text-foreground/90 hover:text-foreground hover:underline hover:underline-offset-2"
588+ onClick = { ( e ) => {
589+ e . stopPropagation ( ) ;
590+ openDiffFileInEditor ( filePath ) ;
591+ } }
592+ onKeyDown = { ( e ) => {
593+ if ( e . key === "Enter" ) {
594+ e . stopPropagation ( ) ;
595+ openDiffFileInEditor ( filePath ) ;
596+ }
597+ } }
598+ >
599+ { filePath }
600+ </ span >
601+ < span
602+ className = { cn (
603+ "shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium leading-none" ,
604+ changeType === "added" &&
605+ "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" ,
606+ changeType === "deleted" &&
607+ "bg-red-500/15 text-red-600 dark:text-red-400" ,
608+ changeType === "renamed" &&
609+ "bg-blue-500/15 text-blue-600 dark:text-blue-400" ,
610+ changeType === "modified" &&
611+ "bg-amber-500/15 text-amber-600 dark:text-amber-400" ,
612+ ) }
613+ >
614+ { changeType === "added"
615+ ? "A"
616+ : changeType === "deleted"
617+ ? "D"
618+ : changeType === "renamed"
619+ ? "R"
620+ : "M" }
621+ </ span >
622+ { isAccepted ? (
623+ < span className = "flex shrink-0 items-center gap-1 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium leading-none text-emerald-600 dark:text-emerald-400" >
624+ < CheckIcon className = "size-3" />
625+ Accepted
626+ </ span >
627+ ) : (
628+ < Button
629+ type = "button"
630+ size = "xs"
631+ variant = "secondary"
632+ onClick = { ( e ) => {
633+ e . stopPropagation ( ) ;
634+ acceptFile ( fileDiff ) ;
635+ } }
636+ >
637+ Accept
638+ </ Button >
639+ ) }
640+ </ button >
641+ { ! isCollapsed && (
642+ < FileDiff
643+ fileDiff = { fileDiff }
644+ options = { {
645+ diffStyle :
646+ diffRenderMode === "split" ? "split" : "unified" ,
647+ lineDiffType : "none" ,
648+ overflow : diffWordWrap ? "wrap" : "scroll" ,
649+ theme : resolveDiffThemeName ( resolvedTheme ) ,
650+ themeType : resolvedTheme as DiffThemeType ,
651+ unsafeCSS : DIFF_PANEL_UNSAFE_CSS ,
652+ } }
653+ />
654+ ) }
534655 </ div >
535- < FileDiff
536- fileDiff = { fileDiff }
537- options = { {
538- diffStyle : diffRenderMode === "split" ? "split" : "unified" ,
539- lineDiffType : "none" ,
540- overflow : diffWordWrap ? "wrap" : "scroll" ,
541- theme : resolveDiffThemeName ( resolvedTheme ) ,
542- themeType : resolvedTheme as DiffThemeType ,
543- unsafeCSS : DIFF_PANEL_UNSAFE_CSS ,
544- } }
545- />
546656 </ div >
547657 ) ;
548658 } ) }
0 commit comments