Skip to content

Commit 9287387

Browse files
committed
Address review feedback on wrap viewport anchoring
1 parent dc9f0cb commit 9287387

1 file changed

Lines changed: 34 additions & 11 deletions

File tree

src/ui/components/panes/DiffPane.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
} from "react";
1111
import type { DiffFile, LayoutMode } from "../../../core/types";
1212
import type { VisibleAgentNote } from "../../lib/agentAnnotations";
13-
import { measureDiffSectionMetrics, type DiffSectionMetrics } from "../../lib/sectionHeights";
13+
import {
14+
measureDiffSectionMetrics,
15+
type DiffSectionMetrics,
16+
type DiffSectionRowMetric,
17+
} from "../../lib/sectionHeights";
1418
import { diffHunkId, diffSectionId } from "../../lib/ids";
1519
import type { AppTheme } from "../../themes";
1620
import { DiffSection } from "./DiffSection";
@@ -25,6 +29,26 @@ interface ViewportRowAnchor {
2529
rowOffsetWithin: number;
2630
}
2731

32+
function binarySearchRowMetric(rowMetrics: DiffSectionRowMetric[], relativeTop: number) {
33+
let low = 0;
34+
let high = rowMetrics.length - 1;
35+
36+
while (low <= high) {
37+
const mid = (low + high) >>> 1;
38+
const rowMetric = rowMetrics[mid]!;
39+
40+
if (relativeTop < rowMetric.offset) {
41+
high = mid - 1;
42+
} else if (relativeTop >= rowMetric.offset + rowMetric.height) {
43+
low = mid + 1;
44+
} else {
45+
return rowMetric;
46+
}
47+
}
48+
49+
return undefined;
50+
}
51+
2852
function findViewportRowAnchor(
2953
files: DiffFile[],
3054
sectionMetrics: DiffSectionMetrics[],
@@ -44,10 +68,7 @@ function findViewportRowAnchor(
4468
const relativeTop = scrollTop - bodyTop;
4569

4670
if (relativeTop >= 0 && relativeTop < bodyHeight && metrics) {
47-
const rowMetric = metrics.rowMetrics.find(
48-
(candidate) =>
49-
relativeTop >= candidate.offset && relativeTop < candidate.offset + candidate.height,
50-
);
71+
const rowMetric = binarySearchRowMetric(metrics.rowMetrics, relativeTop);
5172
if (rowMetric) {
5273
return {
5374
fileId: files[index]!.id,
@@ -407,11 +428,10 @@ export function DiffPane({
407428
const previousFiles = previousFilesRef.current;
408429

409430
if (wrapChanged && previousSectionMetrics && previousFiles.length > 0) {
410-
const previousScrollTop = Math.max(
411-
wrapToggleScrollTop ?? 0,
412-
prevScrollTopRef.current,
413-
scrollViewport.top,
414-
);
431+
const previousScrollTop =
432+
wrapToggleScrollTop != null
433+
? wrapToggleScrollTop
434+
: Math.max(prevScrollTopRef.current, scrollViewport.top);
415435
const anchor = findViewportRowAnchor(
416436
previousFiles,
417437
previousSectionMetrics,
@@ -425,6 +445,8 @@ export function DiffPane({
425445

426446
restoreViewportAnchor();
427447
suppressNextSelectionAutoScrollRef.current = true;
448+
// Retry across a couple of repaint cycles so the restored top-row anchor sticks
449+
// after wrapped row heights and viewport culling settle.
428450
const retryDelays = [0, 16, 48];
429451
const timeouts = retryDelays.map((delay) => setTimeout(restoreViewportAnchor, delay));
430452

@@ -470,7 +492,8 @@ export function DiffPane({
470492
scrollBox.scrollChildIntoView(selectedAnchorId);
471493
};
472494

473-
// Run after this pane renders the selected section/hunk, then retry briefly while layout settles.
495+
// Run after this pane renders the selected section/hunk, then retry briefly while layout
496+
// settles across a couple of repaint cycles.
474497
scrollSelectionIntoView();
475498
const retryDelays = [0, 16, 48];
476499
const timeouts = retryDelays.map((delay) => setTimeout(scrollSelectionIntoView, delay));

0 commit comments

Comments
 (0)