@@ -17,6 +17,20 @@ class Search {
1717 this . results = [ ] ;
1818 this . selectedResultIndex = 0 ;
1919 this . triggerUpdate = true ;
20+ this . debounceTimer = null ;
21+ this . scrollTimer = null ;
22+ // Debounce delays for search updates in ms
23+ this . updateDebounceDelay = 300 ;
24+ this . selectionDebounceDelay = 10 ;
25+ // Debounce delay for scroll updates in ms
26+ this . scrollDebounceDelay = 100 ;
27+ // Height buffer for range of decorations beyond viewport in px
28+ this . decorationBuffer = 500 ;
29+ // Maximum number of decorations to render at once
30+ this . maxDecorations = 500 ;
31+ this . handleScroll = this . _handleScroll . bind ( this ) ;
32+ this . scrollListenerAttached = false ;
33+ this . scrollContainer = null ;
2034 }
2135
2236 focusSelectedResult ( ) {
@@ -76,7 +90,7 @@ class Search {
7690 let result = this . results [ this . selectedResultIndex ] ;
7791 tr . setSelection ( TextSelection . between ( state . doc . resolve ( result . from ) , state . doc . resolve ( result . from ) ) ) ;
7892
79- this . triggerUpdate = true ;
93+ this . triggerDecorations = true ;
8094 dispatch ( tr ) ;
8195
8296 this . focusSelectedResult ( ) ;
@@ -96,7 +110,7 @@ class Search {
96110 let result = this . results [ this . selectedResultIndex ] ;
97111 tr . setSelection ( TextSelection . between ( state . doc . resolve ( result . from ) , state . doc . resolve ( result . from ) ) ) ;
98112
99- this . triggerUpdate = true ;
113+ this . triggerDecorations = true ;
100114 dispatch ( tr ) ;
101115 this . focusSelectedResult ( ) ;
102116 }
@@ -122,12 +136,16 @@ class Search {
122136 if ( node . isText ) {
123137 let chars = removeDiacritics ( node . text ) ;
124138 if ( mergedTextNodes [ index ] ) {
125- let shift = [ ...new Set ( mergedTextNodes [ index ] . chars . map ( x => x [ 0 ] ) ) ] . length ;
126- chars = chars . map ( x => [ x [ 0 ] + shift , x [ 1 ] ] ) ;
127- mergedTextNodes [ index ] . chars = [ ...mergedTextNodes [ index ] . chars , ...chars ] ;
139+ let currentTextNode = mergedTextNodes [ index ] ;
140+ let shift = currentTextNode . textLength ;
141+ for ( let i = 0 ; i < chars . length ; i ++ ) {
142+ chars [ i ] [ 0 ] += shift ;
143+ currentTextNode . chars . push ( chars [ i ] ) ;
144+ }
145+ currentTextNode . textLength += node . text . length ;
128146 }
129147 else {
130- mergedTextNodes [ index ] = { chars, pos } ;
148+ mergedTextNodes [ index ] = { chars, pos, textLength : node . text . length } ;
131149 }
132150 }
133151 else {
@@ -207,39 +225,144 @@ class Search {
207225 if ( tr . docChanged ) {
208226 this . triggerUpdate = true ;
209227 }
228+ else if ( tr . selectionSet && this . results . length ) {
229+ // Update selectedResultIndex based on current selection
230+ let pos = tr . selection . from ;
231+ let index = this . results . findIndex ( r => r . from <= pos && r . to >= pos ) ;
232+ if ( index === - 1 ) {
233+ index = this . results . findIndex ( r => r . from > pos ) ;
234+ if ( index === - 1 ) index = 0 ;
235+ }
236+ if ( index !== this . selectedResultIndex ) {
237+ this . selectedResultIndex = index ;
238+ this . triggerDecorations = true ;
239+ }
240+ }
210241 }
211242 else {
212243 this . decorations = DecorationSet . empty ;
213244 }
214245 }
215246
216247 updateView ( ) {
217- if ( this . triggerUpdate ) {
218- this . triggerUpdate = false ;
219-
220- let { state, dispatch } = this . view ;
221- let { tr } = state ;
222-
223- this . search ( tr . doc ) ;
224-
225- if ( this . triggerFocus && this . results . length ) {
226- this . triggerFocus = false ;
227- let pos = state . selection . from ;
228- let index = this . results . findIndex ( x => x . from >= pos ) ;
229- this . selectedResultIndex = index === - 1 ? 0 : index ;
230- let result = this . results [ this . selectedResultIndex ] ;
231- tr . setSelection ( TextSelection . between ( state . doc . resolve ( result . from ) , state . doc . resolve ( result . from ) ) ) ;
232- this . focusSelectedResult ( ) ;
248+ if ( ! this . active || ! this . searchTerm ) {
249+ // If search is not active, clear decorations
250+ if ( ! this . active && this . decorations !== DecorationSet . empty ) {
251+ this . decorations = DecorationSet . empty ;
252+ let { state, dispatch } = this . view ;
253+ dispatch ( state . tr ) ;
254+ }
255+ return ;
256+ }
257+
258+ if ( this . debounceTimer ) {
259+ clearTimeout ( this . debounceTimer ) ;
260+ }
261+ this . debounceTimer = setTimeout ( ( ) => {
262+ if ( this . triggerUpdate ) {
263+ this . triggerUpdate = false ;
264+ this . triggerDecorations = false ;
265+
266+ let { state, dispatch } = this . view ;
267+ let { tr } = state ;
268+
269+ this . search ( tr . doc ) ;
270+
271+ if ( this . triggerFocus && this . results . length ) {
272+ this . triggerFocus = false ;
273+ let pos = state . selection . from ;
274+ let index = this . results . findIndex ( x => x . from >= pos ) ;
275+ this . selectedResultIndex = index === - 1 ? 0 : index ;
276+ let result = this . results [ this . selectedResultIndex ] ;
277+ tr . setSelection ( TextSelection . between ( state . doc . resolve ( result . from ) , state . doc . resolve ( result . from ) ) ) ;
278+ this . focusSelectedResult ( ) ;
279+ }
280+
281+ this . updateDecorations ( tr . doc ) ;
282+
283+ dispatch ( tr . setMeta ( 'addToHistory' , false ) ) ;
284+ }
285+ else if ( this . triggerDecorations ) {
286+ this . triggerDecorations = false ;
287+ this . updateDecorations ( this . view . state . doc ) ;
288+ this . view . dispatch ( this . view . state . tr . setMeta ( 'addToHistory' , false ) ) ;
289+ }
290+ } , this . triggerUpdate ? this . updateDebounceDelay : this . selectionDebounceDelay ) ;
291+ }
292+
293+ updateScrollListener ( ) {
294+ if ( this . active && ! this . scrollListenerAttached ) {
295+ let container = this . view . dom . closest ( '.editor-core' ) || this . view . dom . parentElement ;
296+ if ( container ) {
297+ container . addEventListener ( 'scroll' , this . _handleScroll ) ;
298+ this . scrollListenerAttached = true ;
299+ this . scrollContainer = container ;
233300 }
234- let list = this . results . map ( ( deco , index ) => (
235- Decoration . inline ( deco . from , deco . to , {
236- class : index === this . selectedResultIndex
237- ? this . findSelectedClass : this . findClass
238- } )
239- ) ) ;
240- this . decorations = DecorationSet . create ( tr . doc , list ) ;
241- dispatch ( tr ) ;
242301 }
302+ else if ( ! this . active && this . scrollListenerAttached ) {
303+ if ( this . scrollContainer ) {
304+ this . scrollContainer . removeEventListener ( 'scroll' , this . _handleScroll ) ;
305+ }
306+ this . scrollListenerAttached = false ;
307+ this . scrollContainer = null ;
308+ }
309+ }
310+
311+ _handleScroll = ( ) => {
312+ if ( ! this . active || ! this . results . length ) return ;
313+
314+ if ( this . scrollTimer ) clearTimeout ( this . scrollTimer ) ;
315+ this . scrollTimer = setTimeout ( ( ) => {
316+ this . updateDecorations ( this . view . state . doc ) ;
317+ this . view . dispatch ( this . view . state . tr . setMeta ( 'addToHistory' , false ) ) ;
318+ } , this . scrollDebounceDelay ) ;
319+ } ;
320+
321+ updateDecorations ( doc ) {
322+ let { view } = this ;
323+ if ( ! view ) return ;
324+
325+ let visibleFrom = 0 ;
326+ let visibleTo = doc . content . size ;
327+
328+ let container = view . dom . closest ( '.editor-core' ) || view . dom . parentElement ;
329+ if ( container ) {
330+ let rect = container . getBoundingClientRect ( ) ;
331+ let editorRect = view . dom . getBoundingClientRect ( ) ;
332+ let left = editorRect . left + ( editorRect . width / 2 ) ;
333+
334+ // Extend decoration range beyond viewport for smoother scrolling
335+ let startObj = view . posAtCoords ( { left, top : rect . top - this . decorationBuffer } ) ;
336+ let endObj = view . posAtCoords ( { left, top : rect . bottom + this . decorationBuffer } ) ;
337+
338+ if ( startObj ) visibleFrom = startObj . pos ;
339+ if ( endObj ) visibleTo = endObj . pos ;
340+ }
341+
342+ let list = [ ] ;
343+
344+ // Always render selected result
345+ if ( this . results [ this . selectedResultIndex ] ) {
346+ let res = this . results [ this . selectedResultIndex ] ;
347+ list . push ( Decoration . inline ( res . from , res . to , { class : this . findSelectedClass } ) ) ;
348+ }
349+
350+ // Render decorations within visible range
351+ let startIndex = this . results . findIndex ( r => r . to >= visibleFrom ) ;
352+ if ( startIndex === - 1 ) startIndex = this . results . length ;
353+
354+ for ( let i = startIndex ; i < this . results . length ; i ++ ) {
355+ let res = this . results [ i ] ;
356+ if ( res . from > visibleTo ) break ;
357+
358+ if ( i === this . selectedResultIndex ) continue ;
359+
360+ list . push ( Decoration . inline ( res . from , res . to , { class : this . findClass } ) ) ;
361+
362+ if ( list . length > this . maxDecorations ) break ;
363+ }
364+
365+ this . decorations = DecorationSet . create ( doc , list ) ;
243366 }
244367}
245368
@@ -260,9 +383,18 @@ export function search() {
260383 view : ( view ) => {
261384 let pluginState = searchKey . getState ( view . state ) ;
262385 pluginState . view = view ;
386+ pluginState . updateScrollListener ( ) ;
387+
263388 return {
264389 update ( view , lastState ) {
390+ let pluginState = searchKey . getState ( view . state ) ;
265391 pluginState . updateView ( view . state , lastState ) ;
392+ pluginState . updateScrollListener ( ) ;
393+ } ,
394+ destroy ( ) {
395+ if ( pluginState . scrollListenerAttached && pluginState . scrollContainer ) {
396+ pluginState . scrollContainer . removeEventListener ( 'scroll' , pluginState . onScroll ) ;
397+ }
266398 }
267399 } ;
268400 } ,
0 commit comments