@@ -10,7 +10,11 @@ import {
1010} from "react" ;
1111import type { DiffFile , LayoutMode } from "../../../core/types" ;
1212import 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" ;
1418import { diffHunkId , diffSectionId } from "../../lib/ids" ;
1519import type { AppTheme } from "../../themes" ;
1620import { 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+
2852function 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