diff --git a/src/ui/components/panes/DiffPane.tsx b/src/ui/components/panes/DiffPane.tsx index ba6fa59..fa49b09 100644 --- a/src/ui/components/panes/DiffPane.tsx +++ b/src/ui/components/panes/DiffPane.tsx @@ -17,10 +17,10 @@ import { type DiffSectionRowMetric, } from "../../lib/sectionHeights"; import { - buildSectionLayoutMetrics, - findHeaderOwningSection, - resolveFileHeaderTop, -} from "../../lib/sectionLayout"; + buildFileSectionLayoutMetrics, + findHeaderOwningFileSection, + resolveFileSectionHeaderTop, +} from "../../lib/fileSectionLayout"; import { diffHunkId, diffSectionId } from "../../lib/ids"; import type { AppTheme } from "../../themes"; import { DiffSection } from "./DiffSection"; @@ -64,13 +64,13 @@ function findViewportRowAnchor( sectionMetrics: DiffSectionMetrics[], scrollTop: number, ) { - const sectionLayoutMetrics = buildSectionLayoutMetrics( + const fileSectionLayoutMetrics = buildFileSectionLayoutMetrics( files, sectionMetrics.map((metrics) => metrics?.bodyHeight ?? 0), ); for (let index = 0; index < files.length; index += 1) { - const layoutMetric = sectionLayoutMetrics[index]; + const layoutMetric = fileSectionLayoutMetrics[index]; const bodyTop = layoutMetric?.bodyTop ?? 0; const metrics = sectionMetrics[index]; const bodyHeight = metrics?.bodyHeight ?? 0; @@ -97,13 +97,13 @@ function resolveViewportRowAnchorTop( sectionMetrics: DiffSectionMetrics[], anchor: ViewportRowAnchor, ) { - const sectionLayoutMetrics = buildSectionLayoutMetrics( + const fileSectionLayoutMetrics = buildFileSectionLayoutMetrics( files, sectionMetrics.map((metrics) => metrics?.bodyHeight ?? 0), ); for (let index = 0; index < files.length; index += 1) { - const layoutMetric = sectionLayoutMetrics[index]; + const layoutMetric = fileSectionLayoutMetrics[index]; const bodyTop = layoutMetric?.bodyTop ?? 0; const file = files[index]; const metrics = sectionMetrics[index]; @@ -287,8 +287,8 @@ export function DiffPane({ () => baseSectionMetrics.map((metrics) => metrics.bodyHeight), [baseSectionMetrics], ); - const baseSectionLayoutMetrics = useMemo( - () => buildSectionLayoutMetrics(files, baseEstimatedBodyHeights), + const baseFileSectionLayoutMetrics = useMemo( + () => buildFileSectionLayoutMetrics(files, baseEstimatedBodyHeights), [baseEstimatedBodyHeights, files], ); @@ -297,11 +297,11 @@ export function DiffPane({ const minVisibleY = Math.max(0, scrollViewport.top - overscanRows); const maxVisibleY = scrollViewport.top + scrollViewport.height + overscanRows; return new Set( - baseSectionLayoutMetrics + baseFileSectionLayoutMetrics .filter((metric) => metric.sectionBottom >= minVisibleY && metric.sectionTop <= maxVisibleY) .map((metric) => metric.fileId), ); - }, [baseSectionLayoutMetrics, scrollViewport.height, scrollViewport.top]); + }, [baseFileSectionLayoutMetrics, scrollViewport.height, scrollViewport.top]); const visibleAgentNotesByFile = useMemo(() => { const next = new Map(); @@ -362,8 +362,8 @@ export function DiffPane({ () => sectionMetrics.map((metrics) => metrics.bodyHeight), [sectionMetrics], ); - const sectionLayoutMetrics = useMemo( - () => buildSectionLayoutMetrics(files, estimatedBodyHeights), + const fileSectionLayoutMetrics = useMemo( + () => buildFileSectionLayoutMetrics(files, estimatedBodyHeights), [estimatedBodyHeights, files], ); // Read the live scroll box position during render so sticky-header ownership flips @@ -371,20 +371,20 @@ export function DiffPane({ const effectiveScrollTop = scrollRef.current?.scrollTop ?? scrollViewport.top; const totalContentHeight = - sectionLayoutMetrics[sectionLayoutMetrics.length - 1]?.sectionBottom ?? 0; + fileSectionLayoutMetrics[fileSectionLayoutMetrics.length - 1]?.sectionBottom ?? 0; const stickyHeaderFile = useMemo(() => { if (files.length < 2) { return null; } - const owner = findHeaderOwningSection(sectionLayoutMetrics, effectiveScrollTop); + const owner = findHeaderOwningFileSection(fileSectionLayoutMetrics, effectiveScrollTop); if (!owner || effectiveScrollTop <= owner.headerTop) { return null; } return files[owner.sectionIndex] ?? null; - }, [effectiveScrollTop, files, sectionLayoutMetrics]); + }, [effectiveScrollTop, fileSectionLayoutMetrics, files]); const visibleWindowedFileIds = useMemo(() => { if (!windowingEnabled) { @@ -418,8 +418,8 @@ export function DiffPane({ return null; } - const selectedSectionLayout = sectionLayoutMetrics[selectedFileIndex]; - if (!selectedSectionLayout) { + const selectedFileSectionLayout = fileSectionLayoutMetrics[selectedFileIndex]; + if (!selectedFileSectionLayout) { return null; } @@ -433,13 +433,19 @@ export function DiffPane({ } return { - top: selectedSectionLayout.bodyTop + hunkBounds.top, + top: selectedFileSectionLayout.bodyTop + hunkBounds.top, height: hunkBounds.height, startRowId: hunkBounds.startRowId, endRowId: hunkBounds.endRowId, - sectionTop: selectedSectionLayout.sectionTop, + sectionTop: selectedFileSectionLayout.sectionTop, }; - }, [sectionLayoutMetrics, sectionMetrics, selectedFile, selectedFileIndex, selectedHunkIndex]); + }, [ + fileSectionLayoutMetrics, + sectionMetrics, + selectedFile, + selectedFileIndex, + selectedHunkIndex, + ]); /** Absolute scroll offset and height of the first inline note in the selected hunk, if any. */ const selectedNoteBounds = useMemo(() => { @@ -483,7 +489,7 @@ export function DiffPane({ /** Scroll one file header to the top using the latest planned section geometry. */ const scrollFileHeaderToTop = useCallback( (fileId: string) => { - const headerTop = resolveFileHeaderTop(sectionLayoutMetrics, fileId); + const headerTop = resolveFileSectionHeaderTop(fileSectionLayoutMetrics, fileId); if (headerTop == null) { return false; } @@ -496,7 +502,7 @@ export function DiffPane({ scrollBox.scrollTo(headerTop); return true; }, - [scrollRef, sectionLayoutMetrics], + [fileSectionLayoutMetrics, scrollRef], ); useLayoutEffect(() => { @@ -585,7 +591,7 @@ export function DiffPane({ if (scrollFileHeaderToTop(pendingFileId)) { clearPendingFileTopAlign(); } - }, [clearPendingFileTopAlign, files, scrollFileHeaderToTop, sectionLayoutMetrics]); + }, [clearPendingFileTopAlign, fileSectionLayoutMetrics, files, scrollFileHeaderToTop]); useLayoutEffect(() => { if (suppressNextSelectionAutoScrollRef.current) { diff --git a/src/ui/lib/sectionLayout.ts b/src/ui/lib/fileSectionLayout.ts similarity index 81% rename from src/ui/lib/sectionLayout.ts rename to src/ui/lib/fileSectionLayout.ts index 4b9fc70..60a732a 100644 --- a/src/ui/lib/sectionLayout.ts +++ b/src/ui/lib/fileSectionLayout.ts @@ -1,7 +1,7 @@ import type { DiffFile } from "../../core/types"; /** Stream geometry for one file section in the main review pane. */ -export interface SectionLayoutMetric { +export interface FileSectionLayoutMetric { fileId: string; sectionIndex: number; sectionTop: number; @@ -12,8 +12,8 @@ export interface SectionLayoutMetric { } /** Build absolute section offsets from file order and measured body heights. */ -export function buildSectionLayoutMetrics(files: DiffFile[], bodyHeights: number[]) { - const metrics: SectionLayoutMetric[] = []; +export function buildFileSectionLayoutMetrics(files: DiffFile[], bodyHeights: number[]) { + const metrics: FileSectionLayoutMetric[] = []; let cursor = 0; files.forEach((file, index) => { @@ -41,7 +41,10 @@ export function buildSectionLayoutMetrics(files: DiffFile[], bodyHeights: number } /** Return the file section that owns the viewport top, switching at each next header row. */ -export function findHeaderOwningSection(sectionMetrics: SectionLayoutMetric[], scrollTop: number) { +export function findHeaderOwningFileSection( + sectionMetrics: FileSectionLayoutMetric[], + scrollTop: number, +) { if (sectionMetrics.length === 0) { return null; } @@ -68,7 +71,10 @@ export function findHeaderOwningSection(sectionMetrics: SectionLayoutMetric[], s } /** Resolve the scroll target needed to make one file header own the viewport top. */ -export function resolveFileHeaderTop(sectionMetrics: SectionLayoutMetric[], fileId: string) { +export function resolveFileSectionHeaderTop( + sectionMetrics: FileSectionLayoutMetric[], + fileId: string, +) { const targetSection = sectionMetrics.find((metric) => metric.fileId === fileId); if (!targetSection) { return null;