@@ -10,8 +10,8 @@ const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
1010 * A class that knows how to search the terminal and how to display the results.
1111 */
1212export class SearchHelper implements ISearchHelper {
13+ private _currentLineMatches : ISearchResult [ ] = [ ] ;
1314 constructor ( private _terminal : ISearchAddonTerminal ) {
14- // TODO: Search for multiple instances on 1 line
1515 // TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
1616 // TODO: Highlight other instances in the viewport
1717 }
@@ -29,29 +29,32 @@ export class SearchHelper implements ISearchHelper {
2929 }
3030
3131 let result : ISearchResult ;
32-
33- let startRow = this . _terminal . _core . buffer . ydisp ;
34- if ( this . _terminal . _core . selectionManager . selectionEnd ) {
35- // Start from the selection end if there is a selection
36- startRow = this . _terminal . _core . selectionManager . selectionEnd [ 1 ] ;
37- }
38-
39- // Search from ydisp + 1 to end
40- for ( let y = startRow + 1 ; y < this . _terminal . _core . buffer . ybase + this . _terminal . rows ; y ++ ) {
41- result = this . _findInLine ( term , y , searchOptions ) ;
42- if ( result ) {
43- break ;
32+ if ( this . _currentLineMatches . length > 0 ) {
33+ result = this . _currentLineMatches . shift ( ) ;
34+ } else {
35+ let startRow = this . _terminal . _core . buffer . ydisp ;
36+ if ( this . _terminal . _core . selectionManager . selectionEnd ) {
37+ // Start from the selection end if there is a selection
38+ startRow = this . _terminal . _core . selectionManager . selectionEnd [ 1 ] ;
4439 }
45- }
4640
47- // Search from the top to the current ydisp
48- if ( ! result ) {
49- for ( let y = 0 ; y < startRow ; y ++ ) {
41+ // Search from ydisp + 1 to end
42+ for ( let y = startRow + 1 ; y < this . _terminal . _core . buffer . ybase + this . _terminal . rows ; y ++ ) {
5043 result = this . _findInLine ( term , y , searchOptions ) ;
5144 if ( result ) {
5245 break ;
5346 }
5447 }
48+
49+ // Search from the top to the current ydisp
50+ if ( ! result ) {
51+ for ( let y = 0 ; y < startRow ; y ++ ) {
52+ result = this . _findInLine ( term , y , searchOptions ) ;
53+ if ( result ) {
54+ break ;
55+ }
56+ }
57+ }
5558 }
5659
5760 // Set selection and scroll if a result was found
@@ -71,29 +74,36 @@ export class SearchHelper implements ISearchHelper {
7174 }
7275
7376 let result : ISearchResult ;
74-
75- let startRow = this . _terminal . _core . buffer . ydisp ;
76- if ( this . _terminal . _core . selectionManager . selectionStart ) {
77- // Start from the selection end if there is a selection
78- startRow = this . _terminal . _core . selectionManager . selectionStart [ 1 ] ;
79- }
80-
81- // Search from ydisp + 1 to end
82- for ( let y = startRow - 1 ; y >= 0 ; y -- ) {
83- result = this . _findInLine ( term , y , searchOptions ) ;
84- if ( result ) {
85- break ;
77+ if ( this . _currentLineMatches . length > 0 ) {
78+ result = this . _currentLineMatches . pop ( ) ;
79+ } else {
80+ let startRow = this . _terminal . _core . buffer . ydisp ;
81+ if ( this . _terminal . _core . selectionManager . selectionStart ) {
82+ // Start from the selection end if there is a selection
83+ startRow = this . _terminal . _core . selectionManager . selectionStart [ 1 ] ;
8684 }
87- }
8885
89- // Search from the top to the current ydisp
90- if ( ! result ) {
91- for ( let y = this . _terminal . _core . buffer . ybase + this . _terminal . rows - 1 ; y > startRow ; y -- ) {
86+ // Search from ydisp + 1 to end
87+ for ( let y = startRow - 1 ; y >= 0 ; y -- ) {
9288 result = this . _findInLine ( term , y , searchOptions ) ;
89+ this . _currentLineMatches . unshift ( result ) ; // Handle backward search
90+ result = this . _currentLineMatches . pop ( ) ;
9391 if ( result ) {
9492 break ;
9593 }
9694 }
95+
96+ // Search from the top to the current ydisp
97+ if ( ! result ) {
98+ for ( let y = this . _terminal . _core . buffer . ybase + this . _terminal . rows - 1 ; y > startRow ; y -- ) {
99+ result = this . _findInLine ( term , y , searchOptions ) ;
100+ this . _currentLineMatches . unshift ( result ) ; // Handle backward search
101+ result = this . _currentLineMatches . pop ( ) ;
102+ if ( result ) {
103+ break ;
104+ }
105+ }
106+ }
97107 }
98108
99109 // Set selection and scroll if a result was found
@@ -128,51 +138,19 @@ export class SearchHelper implements ISearchHelper {
128138
129139 const stringLine = this . translateBufferLineToStringWithWrap ( y , true ) ;
130140 const searchStringLine = searchOptions . caseSensitive ? stringLine : stringLine . toLowerCase ( ) ;
131- const searchTerm = searchOptions . caseSensitive ? term : term . toLowerCase ( ) ;
132- let searchIndex = - 1 ;
133-
134- if ( searchOptions . regex ) {
135- const searchRegex = RegExp ( searchTerm , 'g' ) ;
136- const foundTerm = searchRegex . exec ( searchStringLine ) ;
137- if ( foundTerm && foundTerm [ 0 ] . length > 0 ) {
138- searchIndex = searchRegex . lastIndex - foundTerm [ 0 ] . length ;
139- term = foundTerm [ 0 ] ;
140- }
141- } else {
142- searchIndex = searchStringLine . indexOf ( searchTerm ) ;
143- }
141+ const lineMatch = this . _getNextMatch ( term , searchStringLine , y , searchOptions ) ;
144142
145- if ( searchIndex >= 0 ) {
143+ if ( lineMatch ) {
146144 // Adjust the row number and search index if needed since a "line" of text can span multiple rows
147- if ( searchIndex >= this . _terminal . cols ) {
148- y += Math . floor ( searchIndex / this . _terminal . cols ) ;
149- searchIndex = searchIndex % this . _terminal . cols ;
145+ if ( lineMatch . col >= this . _terminal . cols ) {
146+ lineMatch . row += Math . floor ( lineMatch . col / this . _terminal . cols ) ;
147+ lineMatch . col = lineMatch . col % this . _terminal . cols ;
150148 }
151- if ( searchOptions . wholeWord && ! this . _isWholeWord ( searchIndex , searchStringLine , term ) ) {
149+ if ( searchOptions . wholeWord && ! this . _isWholeWord ( lineMatch . col , searchStringLine , lineMatch . term ) ) {
152150 return ;
153151 }
154-
155- const line = this . _terminal . _core . buffer . lines . get ( y ) ;
156-
157- for ( let i = 0 ; i < searchIndex ; i ++ ) {
158- const charData = line . get ( i ) ;
159- // Adjust the searchIndex to normalize emoji into single chars
160- const char = charData [ 1 /*CHAR_DATA_CHAR_INDEX*/ ] ;
161- if ( char . length > 1 ) {
162- searchIndex -= char . length - 1 ;
163- }
164- // Adjust the searchIndex for empty characters following wide unicode
165- // chars (eg. CJK)
166- const charWidth = charData [ 2 /*CHAR_DATA_WIDTH_INDEX*/ ] ;
167- if ( charWidth === 0 ) {
168- searchIndex ++ ;
169- }
170- }
171- return {
172- term,
173- col : searchIndex ,
174- row : y
175- } ;
152+ this . _normalizeMatch ( lineMatch ) ;
153+ return lineMatch ;
176154 }
177155 }
178156
@@ -212,4 +190,71 @@ export class SearchHelper implements ISearchHelper {
212190 this . _terminal . scrollLines ( result . row - this . _terminal . _core . buffer . ydisp ) ;
213191 return true ;
214192 }
193+
194+ /**
195+ * Returnes an array of matches in the given line.
196+ * @param term The term to search for.
197+ * @param searchString The text to search in.
198+ * @param row The row number.
199+ * @param searchOptions search options,
200+ * @return An array of matches or first match.
201+ */
202+ private _getNextMatch ( term : string , searchString : string , row : number , searchOptions : ISearchOptions ) : ISearchResult {
203+
204+ if ( this . _currentLineMatches . length > 0 && searchOptions . matchMultiple ) {
205+ return this . _currentLineMatches . shift ( ) ;
206+ }
207+
208+ const searchTerm = searchOptions . caseSensitive ? term : term . toLowerCase ( ) ;
209+
210+ let currentIndex = 0 ;
211+ if ( searchOptions . regex ) {
212+ const searchRegex = RegExp ( searchTerm , 'g' ) ;
213+ let foundTerm : RegExpExecArray ;
214+ do {
215+ foundTerm = searchRegex . exec ( searchString ) ;
216+ if ( foundTerm && foundTerm [ 0 ] . length > 0 ) {
217+ term = foundTerm [ 0 ] ;
218+ currentIndex = searchRegex . lastIndex - foundTerm [ 0 ] . length ;
219+ this . _currentLineMatches . push ( { term, col : currentIndex , row} ) ;
220+ searchRegex . lastIndex -= ( term . length - 1 ) ; // Handle regex match overlap
221+ }
222+ } while ( foundTerm && searchOptions . matchMultiple ) ;
223+ } else {
224+ let nextIndex = 0 ;
225+ while ( currentIndex >= 0 ) {
226+ currentIndex = searchString . indexOf ( searchTerm , nextIndex ) ;
227+ if ( currentIndex >= 0 ) {
228+ this . _currentLineMatches . push ( { term , col : currentIndex , row} ) ;
229+ nextIndex = currentIndex + 1 ;
230+ }
231+ if ( ! searchOptions . matchMultiple ) {
232+ break ;
233+ }
234+ }
235+ }
236+
237+ if ( this . _currentLineMatches . length > 0 ) {
238+ return this . _currentLineMatches . shift ( ) ;
239+ }
240+ return undefined ;
241+ }
242+
243+ private _normalizeMatch ( match : ISearchResult ) : void {
244+ const line = this . _terminal . _core . buffer . lines . get ( match . row ) ;
245+ for ( let i = 0 ; i < match . col ; i ++ ) {
246+ const charData = line . get ( i ) ;
247+ // Adjust the searchIndex to normalize emoji into single chars
248+ const char = charData [ 1 /*CHAR_DATA_CHAR_INDEX*/ ] ;
249+ if ( char . length > 1 ) {
250+ match . col -= char . length - 1 ;
251+ }
252+ // Adjust the searchIndex for empty characters following wide unicode
253+ // chars (eg. CJK)
254+ const charWidth = charData [ 2 /*CHAR_DATA_WIDTH_INDEX*/ ] ;
255+ if ( charWidth === 0 ) {
256+ match . col ++ ;
257+ }
258+ }
259+ }
215260}
0 commit comments