1+ import type { IgnorableLintBox } from './Box' ;
12import computeLintBoxes from './computeLintBoxes' ;
23import { isVisible } from './domUtils' ;
34import Highlights from './Highlights' ;
@@ -17,12 +18,16 @@ export default class LintFramework {
1718 private popupHandler : PopupHandler ;
1819 private targets : Set < Node > ;
1920 private scrollableAncestors : Set < HTMLElement > ;
21+ private scrollPositions : Map < HTMLElement , { left : number ; top : number } > ;
2022 private lintRequested = false ;
2123 private renderRequested = false ;
2224 private lastLints : { target : HTMLElement ; lints : UnpackedLint [ ] } [ ] = [ ] ;
25+ private lastBoxes : IgnorableLintBox [ ] = [ ] ;
2326
2427 /** The function to be called to re-render the highlights. This is a variable because it is used to register/deregister event listeners. */
2528 private updateEventCallback : ( ) => void ;
29+ /** Scroll handler used to register/deregister scroll listeners. */
30+ private scrollEventCallback : ( ev : Event ) => void ;
2631
2732 /** Function used to fetch lints for a given text/domain. */
2833 private lintProvider : ( text : string , domain : string ) => Promise < UnpackedLint [ ] > ;
@@ -53,12 +58,18 @@ export default class LintFramework {
5358 } ) ;
5459 this . targets = new Set ( ) ;
5560 this . scrollableAncestors = new Set ( ) ;
61+ this . scrollPositions = new Map ( ) ;
5662 this . lastLints = [ ] ;
63+ this . lastBoxes = [ ] ;
5764
5865 this . updateEventCallback = ( ) => {
5966 this . update ( ) ;
6067 } ;
6168
69+ this . scrollEventCallback = ( ev : Event ) => {
70+ this . onAncestorScroll ( ev ) ;
71+ } ;
72+
6273 const timeoutCallback = ( ) => {
6374 this . update ( ) ;
6475
@@ -159,7 +170,15 @@ export default class LintFramework {
159170 for ( const el of scrollableAncestors ) {
160171 if ( ! this . scrollableAncestors . has ( el as HTMLElement ) ) {
161172 this . scrollableAncestors . add ( el as HTMLElement ) ;
162- ( el as HTMLElement ) . addEventListener ( 'scroll' , this . updateEventCallback , {
173+ // Initialize scroll position tracking
174+ const scroller = el as HTMLElement ;
175+ this . scrollPositions . set ( scroller , {
176+ left : scroller . scrollLeft ,
177+ top : scroller . scrollTop ,
178+ } ) ;
179+
180+ // Listen for scroll with immediate highlight shift
181+ ( el as HTMLElement ) . addEventListener ( 'scroll' , this . scrollEventCallback , {
163182 capture : true ,
164183 passive : true ,
165184 } ) ;
@@ -179,6 +198,47 @@ export default class LintFramework {
179198 }
180199 }
181200
201+ /**
202+ * Handle scrolls on tracked ancestor elements by shifting the last-rendered
203+ * boxes immediately by the scroll delta, then schedule a full recompute.
204+ */
205+ private onAncestorScroll ( ev : Event ) {
206+ const scroller = ev . target as HTMLElement | null ;
207+ if ( ! scroller ) return ;
208+
209+ const prev = this . scrollPositions . get ( scroller ) ;
210+ const current = { left : scroller . scrollLeft , top : scroller . scrollTop } ;
211+ if ( ! prev ) {
212+ this . scrollPositions . set ( scroller , current ) ;
213+ this . updateEventCallback ( ) ;
214+ return ;
215+ }
216+
217+ const dx = current . left - prev . left ;
218+ const dy = current . top - prev . top ;
219+
220+ // Update stored position immediately
221+ this . scrollPositions . set ( scroller , current ) ;
222+
223+ if ( ( dx !== 0 || dy !== 0 ) && this . lastBoxes . length > 0 ) {
224+ // Shift only boxes whose source is within this scroller
225+ const adjusted : IgnorableLintBox [ ] = this . lastBoxes . map ( ( b ) => {
226+ const sourceEl = b . source as any as HTMLElement ;
227+ if ( sourceEl && scroller . contains ( sourceEl ) ) {
228+ return { ...b , x : b . x - dx , y : b . y - dy } ;
229+ }
230+ return b ;
231+ } ) ;
232+
233+ // Render immediately so highlights track content without visible lag
234+ this . highlights . renderLintBoxes ( adjusted ) ;
235+ this . popupHandler . updateLintBoxes ( adjusted ) ;
236+ }
237+
238+ // Continue with normal update to recompute accurate layout
239+ this . updateEventCallback ( ) ;
240+ }
241+
182242 private requestRender ( ) {
183243 if ( this . renderRequested ) {
184244 return ;
@@ -194,6 +254,8 @@ export default class LintFramework {
194254 )
195255 : [ ] ,
196256 ) ;
257+ // Save for immediate scroll adjustments
258+ this . lastBoxes = boxes ;
197259 this . highlights . renderLintBoxes ( boxes ) ;
198260 this . popupHandler . updateLintBoxes ( boxes ) ;
199261
0 commit comments