diff --git a/src/features/git/components/GitDiffPanel.tsx b/src/features/git/components/GitDiffPanel.tsx index 015b69036..cc55a8348 100644 --- a/src/features/git/components/GitDiffPanel.tsx +++ b/src/features/git/components/GitDiffPanel.tsx @@ -168,6 +168,7 @@ export function GitDiffPanel({ gitRootScanLoading = false, gitRootScanError = null, gitRootScanHasScanned = false, + selectedPath = null, onGitRootScanDepthChange, onScanGitRoots, onSelectGitRoot, @@ -461,7 +462,7 @@ export function GitDiffPanel({ return (
onSelectFile?.(file.path)} diff --git a/src/features/git/components/GitDiffViewer.tsx b/src/features/git/components/GitDiffViewer.tsx index 85a9ed51b..c991bd96b 100644 --- a/src/features/git/components/GitDiffViewer.tsx +++ b/src/features/git/components/GitDiffViewer.tsx @@ -80,20 +80,21 @@ const DiffCard = memo(function DiffCard({ }, [entry.diff, entry.path]); return ( -
+
- {entry.status} + + {entry.status} + {entry.path}
{entry.diff.trim().length > 0 && fileDiff ? ( -
+
@@ -109,9 +110,11 @@ export function GitDiffViewer({ selectedPath, isLoading, error, + onActivePathChange, }: GitDiffViewerProps) { const containerRef = useRef(null); const lastScrolledPathRef = useRef(null); + const activePathRef = useRef(null); const poolOptions = useMemo(() => ({ workerFactory }), []); const highlighterOptions = useMemo( () => ({ theme: { dark: "pierre-dark", light: "pierre-light" } }), @@ -131,6 +134,18 @@ export function GitDiffViewer({ overscan: 6, }); const virtualItems = rowVirtualizer.getVirtualItems(); + const stickyEntry = useMemo(() => { + if (!diffs.length) { + return null; + } + if (selectedPath) { + const index = indexByPath.get(selectedPath); + if (index !== undefined) { + return diffs[index]; + } + } + return diffs[0]; + }, [diffs, selectedPath, indexByPath]); useEffect(() => { if (!selectedPath) { @@ -147,12 +162,78 @@ export function GitDiffViewer({ lastScrolledPathRef.current = selectedPath; }, [selectedPath, indexByPath, rowVirtualizer]); + useEffect(() => { + activePathRef.current = selectedPath; + }, [selectedPath]); + + useEffect(() => { + const container = containerRef.current; + if (!container || !onActivePathChange) { + return; + } + let frameId: number | null = null; + + const updateActivePath = () => { + frameId = null; + const items = rowVirtualizer.getVirtualItems(); + if (!items.length) { + return; + } + const scrollTop = container.scrollTop; + const targetOffset = scrollTop + 8; + let activeItem = items[0]; + for (const item of items) { + if (item.start <= targetOffset) { + activeItem = item; + } else { + break; + } + } + const nextPath = diffs[activeItem.index]?.path; + if (!nextPath || nextPath === activePathRef.current) { + return; + } + activePathRef.current = nextPath; + lastScrolledPathRef.current = nextPath; + onActivePathChange(nextPath); + }; + + const handleScroll = () => { + if (frameId !== null) { + return; + } + frameId = requestAnimationFrame(updateActivePath); + }; + + handleScroll(); + container.addEventListener("scroll", handleScroll, { passive: true }); + return () => { + if (frameId !== null) { + cancelAnimationFrame(frameId); + } + container.removeEventListener("scroll", handleScroll); + }; + }, [diffs, onActivePathChange, rowVirtualizer]); + return (
+ {!error && stickyEntry && ( +
+
+ + {stickyEntry.status} + + {stickyEntry.path} +
+
+ )} {error &&
{error}
} {!error && isLoading && diffs.length > 0 && (
@@ -178,7 +259,7 @@ export function GitDiffViewer({ data-index={virtualRow.index} ref={rowVirtualizer.measureElement} style={{ - transform: `translateY(${virtualRow.start}px)`, + transform: `translate3d(0, ${virtualRow.start}px, 0)`, }} >