@@ -8,6 +8,139 @@ const logger = Logger.create('useEditorSearch');
88// Registers a helper CodeMirror extension to be used with
99// useEditorSearchHandler.
1010
11+ type Mark = { clear : ( ) => void } ;
12+ interface SearchHighlightState {
13+ previousKeywordValue : string ;
14+ previousIndex : number ;
15+ previousSearchTimestamp : number ;
16+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied
17+ overlayTimeoutRef : { current : any } ;
18+ clearMarkers ( ) : void ;
19+ clearOverlay ( editor : CodeMirror5Emulation ) : void ;
20+ getSearchTerm ( keyword : string ) : RegExp ;
21+ highlightSearch ( cm : CodeMirror5Emulation , searchTerm : RegExp , index : number , scrollTo : boolean , withSelection : boolean ) : Mark ;
22+ setMarkers ( marker : Mark [ ] ) : void ;
23+ setPreviousIndex ( index : number ) : void ;
24+ setPreviousSearchTimestamp ( timestamp : number ) : void ;
25+ setPreviousKeywordValue ( value : string ) : void ;
26+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied
27+ setScrollbarMarks ( marks : any ) : void ;
28+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied
29+ setOverlay ( overlay : any ) : void ;
30+ setOverlayTimeout ( timeout : number ) : void ;
31+ }
32+
33+
34+ // Modified from codemirror/addons/search/search.js
35+ const searchOverlay = ( query : RegExp ) => {
36+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
37+ return { token : function ( stream : any ) {
38+ query . lastIndex = stream . pos ;
39+ const match = query . exec ( stream . string ) ;
40+ if ( match && match . index === stream . pos ) {
41+ stream . pos += match [ 0 ] . length || 1 ;
42+ return 'search-marker' ;
43+ } else if ( match ) {
44+ stream . pos = match . index ;
45+ } else {
46+ stream . skipToEnd ( ) ;
47+ }
48+ return null ;
49+ } } ;
50+ } ;
51+
52+ const addCodeMirrorExtension = ( CodeMirror : CodeMirror5Emulation ) => {
53+ CodeMirror . defineOption ( 'joplin.search-highlight-state' , null , ( ) => { } ) ;
54+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
55+ CodeMirror ?. defineExtension ( 'setMarkers' , function ( keywords : any , options : any ) {
56+ // Pass arguments in via options to allow the extension to work if multiple editors are open simultaneously
57+ // See https://github.com/laurent22/joplin/issues/13399.
58+ const state : SearchHighlightState = this . getOption ( 'joplin.search-highlight-state' ) ;
59+ if ( ! options ) {
60+ options = { selectedIndex : 0 , searchTimestamp : 0 } ;
61+ }
62+
63+ if ( options . showEditorMarkers === false ) {
64+ state . clearMarkers ( ) ;
65+ state . clearOverlay ( this ) ;
66+ return ;
67+ }
68+
69+ state . clearMarkers ( ) ;
70+
71+ // HIGHLIGHT KEYWORDS
72+ // When doing a global search it's possible to have multiple keywords
73+ // This means we need to highlight each one
74+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
75+ const marks : any = [ ] ;
76+ for ( let i = 0 ; i < keywords . length ; i ++ ) {
77+ const keyword = keywords [ i ] ;
78+
79+ if ( keyword . value === '' ) continue ;
80+
81+ const searchTerm = state . getSearchTerm ( keyword ) ;
82+
83+ // We only want to scroll the first keyword into view in the case of a multi keyword search
84+ const scrollTo = i === 0 && ( state . previousKeywordValue !== keyword . value || state . previousIndex !== options . selectedIndex || options . searchTimestamp !== state . previousSearchTimestamp ) ;
85+
86+ try {
87+ const match = state . highlightSearch ( this , searchTerm , options . selectedIndex , scrollTo , ! ! options . withSelection ) ;
88+ if ( match ) marks . push ( match ) ;
89+ } catch ( error ) {
90+ if ( error . name !== 'SyntaxError' ) {
91+ throw error ;
92+ }
93+ // An error of 'Regular expression too large' might occur in the markJs library
94+ // when the input is really big, this catch is here to avoid the application crashing
95+ // https://github.com/laurent22/joplin/issues/7634
96+ console . error ( 'Error while trying to highlight words from search: ' , error ) ;
97+ }
98+ }
99+
100+ state . setMarkers ( marks ) ;
101+ state . setPreviousIndex ( options . selectedIndex ) ;
102+ state . setPreviousSearchTimestamp ( options . searchTimestamp ) ;
103+
104+ // SEARCHOVERLAY
105+ // We only want to highlight all matches when there is only 1 search term
106+ if ( keywords . length !== 1 || keywords [ 0 ] . value === '' ) {
107+ state . clearOverlay ( this ) ;
108+ const prev = keywords . length > 1 ? keywords [ 0 ] . value : '' ;
109+ state . setPreviousKeywordValue ( prev ) ;
110+ return 0 ;
111+ }
112+
113+ const searchTerm = state . getSearchTerm ( keywords [ 0 ] ) ;
114+
115+ // Determine the number of matches in the source, this is passed on
116+ // to the NoteEditor component
117+ const regexMatches = this . getValue ( ) . match ( searchTerm ) ;
118+ const nMatches = regexMatches ? regexMatches . length : 0 ;
119+
120+ // Don't bother clearing and re-calculating the overlay if the search term
121+ // hasn't changed
122+ if ( keywords [ 0 ] . value === state . previousKeywordValue ) return nMatches ;
123+
124+ state . clearOverlay ( this ) ;
125+ state . setPreviousKeywordValue ( keywords [ 0 ] . value ) ;
126+
127+ // These operations are pretty slow, so we won't add use them until the user
128+ // has finished typing, 500ms is probably enough time
129+ const timeout = shim . setTimeout ( ( ) => {
130+ const scrollMarks = this . showMatchesOnScrollbar ?.( searchTerm , true , 'cm-search-marker-scrollbar' ) ;
131+ const overlay = searchOverlay ( searchTerm ) ;
132+ this . addOverlay ( overlay ) ;
133+ state . setOverlay ( overlay ) ;
134+ state . setScrollbarMarks ( scrollMarks ) ;
135+ } , 500 ) ;
136+
137+ state . setOverlayTimeout ( timeout ) ;
138+ state . overlayTimeoutRef . current = timeout ;
139+
140+ return nMatches ;
141+ } ) ;
142+ } ;
143+
11144export default function useEditorSearchExtension ( CodeMirror : CodeMirror5Emulation ) {
12145
13146 const [ markers , setMarkers ] = useState ( [ ] ) ;
@@ -48,23 +181,6 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
48181 setOverlayTimeout ( null ) ;
49182 } , [ scrollbarMarks , overlay , overlayTimeout ] ) ;
50183
51- // Modified from codemirror/addons/search/search.js
52- const searchOverlay = useCallback ( ( query : RegExp ) => {
53- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
54- return { token : function ( stream : any ) {
55- query . lastIndex = stream . pos ;
56- const match = query . exec ( stream . string ) ;
57- if ( match && match . index === stream . pos ) {
58- stream . pos += match [ 0 ] . length || 1 ;
59- return 'search-marker' ;
60- } else if ( match ) {
61- stream . pos = match . index ;
62- } else {
63- stream . skipToEnd ( ) ;
64- }
65- return null ;
66- } } ;
67- } , [ ] ) ;
68184
69185 // Highlights the currently active found work
70186 // It's possible to get tricky with this functions and just use findNext/findPrev
@@ -115,89 +231,25 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
115231 } ;
116232 } , [ ] ) ;
117233
118- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
119- CodeMirror ?. defineExtension ( 'setMarkers' , function ( keywords : any , options : any ) {
120- if ( ! options ) {
121- options = { selectedIndex : 0 , searchTimestamp : 0 } ;
122- }
123-
124- if ( options . showEditorMarkers === false ) {
125- clearMarkers ( ) ;
126- clearOverlay ( this ) ;
127- return ;
128- }
129-
130- clearMarkers ( ) ;
131-
132- // HIGHLIGHT KEYWORDS
133- // When doing a global search it's possible to have multiple keywords
134- // This means we need to highlight each one
135- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
136- const marks : any = [ ] ;
137- for ( let i = 0 ; i < keywords . length ; i ++ ) {
138- const keyword = keywords [ i ] ;
139-
140- if ( keyword . value === '' ) continue ;
141-
142- const searchTerm = getSearchTerm ( keyword ) ;
143-
144- // We only want to scroll the first keyword into view in the case of a multi keyword search
145- const scrollTo = i === 0 && ( previousKeywordValue !== keyword . value || previousIndex !== options . selectedIndex || options . searchTimestamp !== previousSearchTimestamp ) ;
146-
147- try {
148- const match = highlightSearch ( this , searchTerm , options . selectedIndex , scrollTo , ! ! options . withSelection ) ;
149- if ( match ) marks . push ( match ) ;
150- } catch ( error ) {
151- if ( error . name !== 'SyntaxError' ) {
152- throw error ;
153- }
154- // An error of 'Regular expression too large' might occur in the markJs library
155- // when the input is really big, this catch is here to avoid the application crashing
156- // https://github.com/laurent22/joplin/issues/7634
157- console . error ( 'Error while trying to highlight words from search: ' , error ) ;
158- }
159- }
160-
161- setMarkers ( marks ) ;
162- setPreviousIndex ( options . selectedIndex ) ;
163- setPreviousSearchTimestamp ( options . searchTimestamp ) ;
164-
165- // SEARCHOVERLAY
166- // We only want to highlight all matches when there is only 1 search term
167- if ( keywords . length !== 1 || keywords [ 0 ] . value === '' ) {
168- clearOverlay ( this ) ;
169- const prev = keywords . length > 1 ? keywords [ 0 ] . value : '' ;
170- setPreviousKeywordValue ( prev ) ;
171- return 0 ;
172- }
173-
174- const searchTerm = getSearchTerm ( keywords [ 0 ] ) ;
175-
176- // Determine the number of matches in the source, this is passed on
177- // to the NoteEditor component
178- const regexMatches = this . getValue ( ) . match ( searchTerm ) ;
179- const nMatches = regexMatches ? regexMatches . length : 0 ;
180-
181- // Don't bother clearing and re-calculating the overlay if the search term
182- // hasn't changed
183- if ( keywords [ 0 ] . value === previousKeywordValue ) return nMatches ;
184-
185- clearOverlay ( this ) ;
186- setPreviousKeywordValue ( keywords [ 0 ] . value ) ;
187-
188- // These operations are pretty slow, so we won't add use them until the user
189- // has finished typing, 500ms is probably enough time
190- const timeout = shim . setTimeout ( ( ) => {
191- const scrollMarks = this . showMatchesOnScrollbar ?.( searchTerm , true , 'cm-search-marker-scrollbar' ) ;
192- const overlay = searchOverlay ( searchTerm ) ;
193- this . addOverlay ( overlay ) ;
194- setOverlay ( overlay ) ;
195- setScrollbarMarks ( scrollMarks ) ;
196- } , 500 ) ;
197-
198- setOverlayTimeout ( timeout ) ;
199- overlayTimeoutRef . current = timeout ;
200-
201- return nMatches ;
202- } ) ;
234+ if ( CodeMirror ) {
235+ const state : SearchHighlightState = {
236+ previousKeywordValue,
237+ previousIndex,
238+ previousSearchTimestamp,
239+ overlayTimeoutRef,
240+ clearMarkers,
241+ clearOverlay,
242+ getSearchTerm,
243+ highlightSearch,
244+ setMarkers,
245+ setPreviousIndex,
246+ setPreviousSearchTimestamp,
247+ setPreviousKeywordValue,
248+ setScrollbarMarks,
249+ setOverlay,
250+ setOverlayTimeout,
251+ } ;
252+ addCodeMirrorExtension ( CodeMirror ) ;
253+ CodeMirror . setOption ( 'joplin.search-highlight-state' , state ) ;
254+ }
203255}
0 commit comments