11import React , { useMemo , useRef , useEffect , useLayoutEffect , useCallback , useState } from 'react' ;
2- import { FileDiff } from '@pierre/diffs/react' ;
2+ import { FileDiff , type DiffLineAnnotation } from '@pierre/diffs/react' ;
33import { getSingularPatch , processFile } from '@pierre/diffs' ;
44import { CodeAnnotation , CodeAnnotationType , SelectedLineRange , DiffAnnotationMetadata } from '@plannotator/ui/types' ;
55import { useTheme } from '@plannotator/ui/components/ThemeProvider' ;
@@ -22,10 +22,86 @@ import {
2222 swapActiveSearchHighlight ,
2323} from '../utils/reviewSearchHighlight' ;
2424
25+ interface PierreDiffContentProps {
26+ filePath : string ;
27+ fileDiff : ReturnType < typeof getSingularPatch > ;
28+ pierreTheme : { type : 'dark' | 'light' ; css : string } ;
29+ diffStyle : 'split' | 'unified' ;
30+ diffOverflow ?: 'scroll' | 'wrap' ;
31+ diffIndicators ?: 'bars' | 'classic' | 'none' ;
32+ lineDiffType ?: 'word-alt' | 'word' | 'char' | 'none' ;
33+ disableLineNumbers ?: boolean ;
34+ disableBackground ?: boolean ;
35+ mergedAnnotations : DiffLineAnnotation < DiffAnnotationMetadata > [ ] ;
36+ pendingSelection : SelectedLineRange | null ;
37+ onLineSelectionEnd : ( range : SelectedLineRange | null ) => void ;
38+ renderAnnotation : ( annotation : { side : string ; lineNumber : number ; metadata ?: DiffAnnotationMetadata } ) => React . ReactNode ;
39+ renderHoverUtility : ( getHoveredLine : ( ) => { lineNumber : number ; side : 'deletions' | 'additions' } | undefined ) => React . ReactNode ;
40+ }
41+
42+ const PierreDiffContent = React . memo ( ( {
43+ filePath,
44+ fileDiff,
45+ pierreTheme,
46+ diffStyle,
47+ diffOverflow,
48+ diffIndicators,
49+ lineDiffType,
50+ disableLineNumbers,
51+ disableBackground,
52+ mergedAnnotations,
53+ pendingSelection,
54+ onLineSelectionEnd,
55+ renderAnnotation,
56+ renderHoverUtility,
57+ } : PierreDiffContentProps ) => {
58+ return (
59+ < FileDiff
60+ key = { filePath }
61+ fileDiff = { fileDiff }
62+ options = { {
63+ themeType : pierreTheme . type ,
64+ unsafeCSS : pierreTheme . css ,
65+ diffStyle,
66+ overflow : diffOverflow ,
67+ diffIndicators,
68+ lineDiffType,
69+ disableLineNumbers,
70+ disableBackground,
71+ hunkSeparators : 'line-info' ,
72+ enableLineSelection : true ,
73+ enableHoverUtility : true ,
74+ onLineSelectionEnd,
75+ } }
76+ lineAnnotations = { mergedAnnotations }
77+ selectedLines = { pendingSelection || undefined }
78+ renderAnnotation = { renderAnnotation }
79+ renderHoverUtility = { renderHoverUtility }
80+ />
81+ ) ;
82+ } , ( prev , next ) => (
83+ prev . filePath === next . filePath &&
84+ prev . fileDiff === next . fileDiff &&
85+ prev . pierreTheme . type === next . pierreTheme . type &&
86+ prev . pierreTheme . css === next . pierreTheme . css &&
87+ prev . diffStyle === next . diffStyle &&
88+ prev . diffOverflow === next . diffOverflow &&
89+ prev . diffIndicators === next . diffIndicators &&
90+ prev . lineDiffType === next . lineDiffType &&
91+ prev . disableLineNumbers === next . disableLineNumbers &&
92+ prev . disableBackground === next . disableBackground &&
93+ prev . mergedAnnotations === next . mergedAnnotations &&
94+ prev . pendingSelection === next . pendingSelection &&
95+ prev . onLineSelectionEnd === next . onLineSelectionEnd &&
96+ prev . renderAnnotation === next . renderAnnotation &&
97+ prev . renderHoverUtility === next . renderHoverUtility
98+ ) ) ;
99+
25100interface DiffViewerProps {
26101 patch : string ;
27102 filePath : string ;
28103 oldPath ?: string ;
104+ isFocused ?: boolean ;
29105 diffStyle : 'split' | 'unified' ;
30106 diffOverflow ?: 'scroll' | 'wrap' ;
31107 diffIndicators ?: 'bars' | 'classic' | 'none' ;
@@ -69,6 +145,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
69145 patch,
70146 filePath,
71147 oldPath,
148+ isFocused = false ,
72149 diffStyle,
73150 diffOverflow,
74151 diffIndicators = 'bars' ,
@@ -107,6 +184,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
107184} ) => {
108185 const { theme, colorTheme, resolvedMode } = useTheme ( ) ;
109186 const containerRef = useRef < HTMLDivElement > ( null ) ;
187+ const splitSurfaceRef = useRef < HTMLDivElement > ( null ) ;
110188 const [ fileCommentAnchor , setFileCommentAnchor ] = useState < HTMLElement | null > ( null ) ;
111189
112190 // Resizable split pane — only applies when Pierre renders a two-column grid
@@ -134,13 +212,13 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
134212
135213 const handleSplitDragStart = useCallback ( ( e : React . PointerEvent ) => {
136214 e . preventDefault ( ) ;
137- const container = containerRef . current ;
138- if ( ! container ) return ;
139- const rect = container . getBoundingClientRect ( ) ;
215+ if ( ! splitSurfaceRef . current ) return ;
140216 setIsDraggingSplit ( true ) ;
141217
142- const onMove = ( e : PointerEvent ) => {
143- const ratio = ( e . clientX - rect . left ) / rect . width ;
218+ const onMove = ( moveEvent : PointerEvent ) => {
219+ const rect = splitSurfaceRef . current ?. getBoundingClientRect ( ) ;
220+ if ( ! rect || rect . width <= 0 ) return ;
221+ const ratio = ( moveEvent . clientX - rect . left ) / rect . width ;
144222 setSplitRatio ( Math . min ( 0.8 , Math . max ( 0.2 , ratio ) ) ) ;
145223 } ;
146224
@@ -160,7 +238,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
160238 storage . setItem ( 'review-split-ratio' , '0.5' ) ;
161239 } , [ ] ) ;
162240
163- const toolbar = useAnnotationToolbar ( { patch, filePath, onLineSelection, onAddAnnotation, onEditAnnotation } ) ;
241+ const toolbar = useAnnotationToolbar ( { patch, filePath, isFocused , onLineSelection, onAddAnnotation, onEditAnnotation } ) ;
164242
165243 // Parse patch into FileDiffMetadata for @pierre /diffs FileDiff component
166244 const fileDiff = useMemo ( ( ) => getSingularPatch ( patch ) , [ patch ] ) ;
@@ -396,14 +474,30 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
396474 [data-column-number] { background-color: ${ bg } !important; }
397475 [data-diffs-header] [data-title] { display: none !important; }
398476 [data-diff-type='split'][data-overflow='scroll'] {
399- grid-template-columns: var(--split-left, 1fr) var(--split-right, 1fr) !important;
477+ grid-template-columns:
478+ minmax(0, var(--split-left, 1fr))
479+ minmax(0, var(--split-right, 1fr)) !important;
480+ }
481+ [data-diff-type='split'][data-overflow='scroll'] > [data-code][data-deletions],
482+ [data-diff-type='split'][data-overflow='scroll'] > [data-code][data-additions],
483+ [data-diff-type='split'][data-overflow='scroll'] > [data-code][data-deletions] [data-content],
484+ [data-diff-type='split'][data-overflow='scroll'] > [data-code][data-additions] [data-content] {
485+ min-width: 0 !important;
400486 }
401487 ${ fontCSS }
402488 ` ,
403489 } ) ;
404490 } ) ;
405491 } , [ resolvedMode , colorTheme , fontFamily , fontSize ] ) ;
406492
493+ const splitGridStyle = useMemo ( ( ) => {
494+ if ( ! isSplitLayout || diffOverflow === 'wrap' ) return undefined ;
495+ return {
496+ '--split-left' : `${ splitRatio } fr` ,
497+ '--split-right' : `${ 1 - splitRatio } fr` ,
498+ } as React . CSSProperties ;
499+ } , [ diffOverflow , isSplitLayout , splitRatio ] ) ;
500+
407501 return (
408502 < div className = "h-full flex flex-col" >
409503 < FileHeader
@@ -420,46 +514,36 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
420514 />
421515
422516 < div ref = { containerRef } className = { `flex-1 overflow-auto relative ${ isDraggingSplit ? 'select-none' : '' } ` } onMouseMove = { toolbar . handleMouseMove } >
423- { isSplitLayout && diffOverflow !== 'wrap' && (
424- < div
425- className = "absolute top-0 bottom-0 z-10 cursor-col-resize group"
426- style = { { left : `${ splitRatio * 100 } %` , width : 9 , marginLeft : - 4 } }
427- onPointerDown = { handleSplitDragStart }
428- onDoubleClick = { resetSplitRatio }
429- >
430- < div className = "absolute inset-y-0 left-1/2 w-px bg-border group-hover:bg-primary/50 group-active:bg-primary/70 transition-colors" />
517+ < div className = "p-4" >
518+ < div ref = { splitSurfaceRef } className = "relative min-w-0" style = { splitGridStyle } >
519+ { isSplitLayout && diffOverflow !== 'wrap' && (
520+ < div
521+ className = "absolute top-0 bottom-0 z-10 cursor-col-resize group"
522+ style = { { left : `${ splitRatio * 100 } %` , width : 9 , marginLeft : - 4 } }
523+ onPointerDown = { handleSplitDragStart }
524+ onDoubleClick = { resetSplitRatio }
525+ >
526+ < div className = "pointer-events-none absolute inset-y-0 left-1/2 -translate-x-1/2 w-px bg-border transition-[width,background-color] group-hover:w-0.5 group-hover:bg-primary/50 group-active:w-0.5 group-active:bg-primary/70" />
527+ </ div >
528+ ) }
529+ < PierreDiffContent
530+ filePath = { filePath }
531+ fileDiff = { augmentedDiff }
532+ pierreTheme = { pierreTheme }
533+ diffStyle = { diffStyle }
534+ diffOverflow = { diffOverflow }
535+ diffIndicators = { diffIndicators }
536+ lineDiffType = { lineDiffType }
537+ disableLineNumbers = { disableLineNumbers }
538+ disableBackground = { disableBackground }
539+ mergedAnnotations = { mergedAnnotations }
540+ pendingSelection = { pendingSelection }
541+ onLineSelectionEnd = { toolbar . handleLineSelectionEnd }
542+ renderAnnotation = { renderAnnotation }
543+ renderHoverUtility = { renderHoverUtility }
544+ />
545+ </ div >
431546 </ div >
432- ) }
433- < div
434- className = "p-4"
435- style = { isSplitLayout && diffOverflow !== 'wrap' ? {
436- '--split-left' : `${ splitRatio } fr` ,
437- '--split-right' : `${ 1 - splitRatio } fr` ,
438- } as React . CSSProperties : undefined }
439- >
440- < FileDiff
441- key = { filePath }
442- fileDiff = { augmentedDiff }
443- options = { {
444- themeType : pierreTheme . type ,
445- unsafeCSS : pierreTheme . css ,
446- diffStyle,
447- overflow : diffOverflow ,
448- diffIndicators,
449- lineDiffType,
450- disableLineNumbers,
451- disableBackground,
452- hunkSeparators : 'line-info' ,
453- enableLineSelection : true ,
454- enableHoverUtility : true ,
455- onLineSelectionEnd : toolbar . handleLineSelectionEnd ,
456- } }
457- lineAnnotations = { mergedAnnotations }
458- selectedLines = { pendingSelection || undefined }
459- renderAnnotation = { renderAnnotation }
460- renderHoverUtility = { renderHoverUtility }
461- />
462- </ div >
463547
464548 { toolbar . toolbarState && (
465549 < AnnotationToolbar
0 commit comments