33 * @license MIT
44 */
55
6- import { Terminal , IDisposable , ITerminalAddon , ISelectionPosition } from 'xterm' ;
6+ import { Terminal , IBufferLine , IDisposable , ITerminalAddon , ISelectionPosition } from 'xterm' ;
77
88export interface ISearchOptions {
99 regex ?: boolean ;
@@ -21,8 +21,20 @@ export interface ISearchResult {
2121 term : string ;
2222 col : number ;
2323 row : number ;
24+ size : number ;
2425}
2526
27+ type LineCacheEntry = [
28+ /**
29+ * The string representation of a line (as opposed to the buffer cell representation).
30+ */
31+ lineAsString : string ,
32+ /**
33+ * The offsets where each line starts when the entry describes a wrapped line.
34+ */
35+ lineOffsets : number [ ]
36+ ] ;
37+
2638const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?' ;
2739const LINES_CACHE_TIME_TO_LIVE = 15 * 1000 ; // 15 secs
2840
@@ -34,7 +46,7 @@ export class SearchAddon implements ITerminalAddon {
3446 * We memoize the calls into an array that has a time based ttl.
3547 * _linesCache is also invalidated when the terminal cursor moves.
3648 */
37- private _linesCache : string [ ] | undefined ;
49+ private _linesCache : LineCacheEntry [ ] | undefined ;
3850 private _linesCacheTimeoutId = 0 ;
3951 private _cursorMoveListener : IDisposable | undefined ;
4052 private _resizeListener : IDisposable | undefined ;
@@ -257,7 +269,7 @@ export class SearchAddon implements ITerminalAddon {
257269 */
258270 protected _findInLine ( term : string , searchPosition : ISearchPosition , searchOptions : ISearchOptions = { } , isReverseSearch : boolean = false ) : ISearchResult | undefined {
259271 const terminal = this . _terminal ! ;
260- let row = searchPosition . startRow ;
272+ const row = searchPosition . startRow ;
261273 const col = searchPosition . startCol ;
262274
263275 // Ignore wrapped lines, only consider on unwrapped line (first row of command string).
@@ -274,14 +286,16 @@ export class SearchAddon implements ITerminalAddon {
274286 searchPosition . startCol += terminal . cols ;
275287 return this . _findInLine ( term , searchPosition , searchOptions ) ;
276288 }
277- let stringLine = this . _linesCache ? this . _linesCache [ row ] : void 0 ;
278- if ( stringLine === void 0 ) {
279- stringLine = this . _translateBufferLineToStringWithWrap ( row , true ) ;
289+ let cache = this . _linesCache ?. [ row ] ;
290+ if ( ! cache ) {
291+ cache = this . _translateBufferLineToStringWithWrap ( row , true ) ;
280292 if ( this . _linesCache ) {
281- this . _linesCache [ row ] = stringLine ;
293+ this . _linesCache [ row ] = cache ;
282294 }
283295 }
296+ const [ stringLine , offsets ] = cache ;
284297
298+ const offset = this . _bufferColsToStringOffset ( row , col ) ;
285299 const searchTerm = searchOptions . caseSensitive ? term : term . toLowerCase ( ) ;
286300 const searchStringLine = searchOptions . caseSensitive ? stringLine : stringLine . toLowerCase ( ) ;
287301
@@ -290,68 +304,108 @@ export class SearchAddon implements ITerminalAddon {
290304 const searchRegex = RegExp ( searchTerm , 'g' ) ;
291305 let foundTerm : RegExpExecArray | null ;
292306 if ( isReverseSearch ) {
293- // This loop will get the resultIndex of the _last_ regex match in the range 0..col
294- while ( foundTerm = searchRegex . exec ( searchStringLine . slice ( 0 , col ) ) ) {
307+ // This loop will get the resultIndex of the _last_ regex match in the range 0..offset
308+ while ( foundTerm = searchRegex . exec ( searchStringLine . slice ( 0 , offset ) ) ) {
295309 resultIndex = searchRegex . lastIndex - foundTerm [ 0 ] . length ;
296310 term = foundTerm [ 0 ] ;
297311 searchRegex . lastIndex -= ( term . length - 1 ) ;
298312 }
299313 } else {
300- foundTerm = searchRegex . exec ( searchStringLine . slice ( col ) ) ;
314+ foundTerm = searchRegex . exec ( searchStringLine . slice ( offset ) ) ;
301315 if ( foundTerm && foundTerm [ 0 ] . length > 0 ) {
302- resultIndex = col + ( searchRegex . lastIndex - foundTerm [ 0 ] . length ) ;
316+ resultIndex = offset + ( searchRegex . lastIndex - foundTerm [ 0 ] . length ) ;
303317 term = foundTerm [ 0 ] ;
304318 }
305319 }
306320 } else {
307321 if ( isReverseSearch ) {
308- if ( col - searchTerm . length >= 0 ) {
309- resultIndex = searchStringLine . lastIndexOf ( searchTerm , col - searchTerm . length ) ;
322+ if ( offset - searchTerm . length >= 0 ) {
323+ resultIndex = searchStringLine . lastIndexOf ( searchTerm , offset - searchTerm . length ) ;
310324 }
311325 } else {
312- resultIndex = searchStringLine . indexOf ( searchTerm , col ) ;
326+ resultIndex = searchStringLine . indexOf ( searchTerm , offset ) ;
313327 }
314328 }
315329
316330 if ( resultIndex >= 0 ) {
317- // Adjust the row number and search index if needed since a "line" of text can span multiple rows
318- if ( resultIndex >= terminal . cols ) {
319- row += Math . floor ( resultIndex / terminal . cols ) ;
320- resultIndex = resultIndex % terminal . cols ;
321- }
322331 if ( searchOptions . wholeWord && ! this . _isWholeWord ( resultIndex , searchStringLine , term ) ) {
323332 return ;
324333 }
325334
326- const line = terminal . buffer . active . getLine ( row ) ;
327-
328- if ( line ) {
329- for ( let i = 0 ; i < resultIndex ; i ++ ) {
330- const cell = line . getCell ( i ) ;
331- if ( ! cell ) {
332- break ;
333- }
334- // Adjust the searchIndex to normalize emoji into single chars
335- const char = cell . getChars ( ) ;
336- if ( char . length > 1 ) {
337- resultIndex -= char . length - 1 ;
338- }
339- // Adjust the searchIndex for empty characters following wide unicode
340- // chars (eg. CJK)
341- const charWidth = cell . getWidth ( ) ;
342- if ( charWidth === 0 ) {
343- resultIndex ++ ;
344- }
345- }
335+ // Adjust the row number and search index if needed since a "line" of text can span multiple rows
336+ let startRowOffset = 0 ;
337+ while ( startRowOffset < offsets . length - 1 && resultIndex >= offsets [ startRowOffset + 1 ] ) {
338+ startRowOffset ++ ;
346339 }
340+ let endRowOffset = startRowOffset ;
341+ while ( endRowOffset < offsets . length - 1 && resultIndex + term . length >= offsets [ endRowOffset + 1 ] ) {
342+ endRowOffset ++ ;
343+ }
344+ const startColOffset = resultIndex - offsets [ startRowOffset ] ;
345+ const endColOffset = resultIndex + term . length - offsets [ endRowOffset ] ;
346+ const startColIndex = this . _stringLengthToBufferSize ( row + startRowOffset , startColOffset ) ;
347+ const endColIndex = this . _stringLengthToBufferSize ( row + endRowOffset , endColOffset ) ;
348+ const size = endColIndex - startColIndex + terminal . cols * ( endRowOffset - startRowOffset ) ;
349+
347350 return {
348351 term,
349- col : resultIndex ,
350- row
352+ col : startColIndex ,
353+ row : row + startRowOffset ,
354+ size
351355 } ;
352356 }
353357 }
354358
359+ private _stringLengthToBufferSize ( row : number , offset : number ) : number {
360+ const line = this . _terminal ! . buffer . active . getLine ( row ) ;
361+ if ( ! line ) {
362+ return 0 ;
363+ }
364+ for ( let i = 0 ; i < offset ; i ++ ) {
365+ const cell = line . getCell ( i ) ;
366+ if ( ! cell ) {
367+ break ;
368+ }
369+ // Adjust the searchIndex to normalize emoji into single chars
370+ const char = cell . getChars ( ) ;
371+ if ( char . length > 1 ) {
372+ offset -= char . length - 1 ;
373+ }
374+ // Adjust the searchIndex for empty characters following wide unicode
375+ // chars (eg. CJK)
376+ const nextCell = line . getCell ( i + 1 ) ;
377+ if ( nextCell && nextCell . getWidth ( ) === 0 ) {
378+ offset ++ ;
379+ }
380+ }
381+ return offset ;
382+ }
383+
384+ private _bufferColsToStringOffset ( startRow : number , cols : number ) : number {
385+ const terminal = this . _terminal ! ;
386+ let lineIndex = startRow ;
387+ let offset = 0 ;
388+ let line = terminal . buffer . active . getLine ( lineIndex ) ;
389+ while ( cols > 0 && line ) {
390+ for ( let i = 0 ; i < cols && i < terminal . cols ; i ++ ) {
391+ const cell = line . getCell ( i ) ;
392+ if ( ! cell ) {
393+ break ;
394+ }
395+ if ( cell . getWidth ( ) ) {
396+ offset += cell . getChars ( ) . length ;
397+ }
398+ }
399+ lineIndex ++ ;
400+ line = terminal . buffer . active . getLine ( lineIndex ) ;
401+ if ( line && ! line . isWrapped ) {
402+ break ;
403+ }
404+ cols -= terminal . cols ;
405+ }
406+ return offset ;
407+ }
408+
355409 /**
356410 * Translates a buffer line to a string, including subsequent lines if they are wraps.
357411 * Wide characters will count as two columns in the resulting string. This
@@ -360,23 +414,33 @@ export class SearchAddon implements ITerminalAddon {
360414 * @param line The line being translated.
361415 * @param trimRight Whether to trim whitespace to the right.
362416 */
363- private _translateBufferLineToStringWithWrap ( lineIndex : number , trimRight : boolean ) : string {
417+ private _translateBufferLineToStringWithWrap ( lineIndex : number , trimRight : boolean ) : LineCacheEntry {
364418 const terminal = this . _terminal ! ;
365- let lineString = '' ;
366- let lineWrapsToNext : boolean ;
367-
368- do {
419+ const strings = [ ] ;
420+ const lineOffsets = [ 0 ] ;
421+ let line = terminal . buffer . active . getLine ( lineIndex ) ;
422+ while ( line ) {
369423 const nextLine = terminal . buffer . active . getLine ( lineIndex + 1 ) ;
370- lineWrapsToNext = nextLine ? nextLine . isWrapped : false ;
371- const line = terminal . buffer . active . getLine ( lineIndex ) ;
372- if ( ! line ) {
424+ const lineWrapsToNext = nextLine ? nextLine . isWrapped : false ;
425+ let string = line . translateToString ( ! lineWrapsToNext && trimRight ) ;
426+ if ( lineWrapsToNext && nextLine ) {
427+ const lastCell = line . getCell ( line . length - 1 ) ;
428+ const lastCellIsNull = lastCell && lastCell . getCode ( ) === 0 && lastCell . getWidth ( ) === 1 ;
429+ // a wide character wrapped to the next line
430+ if ( lastCellIsNull && nextLine . getCell ( 0 ) ?. getWidth ( ) === 2 ) {
431+ string = string . slice ( 0 , - 1 ) ;
432+ }
433+ }
434+ strings . push ( string ) ;
435+ if ( lineWrapsToNext ) {
436+ lineOffsets . push ( lineOffsets [ lineOffsets . length - 1 ] + string . length ) ;
437+ } else {
373438 break ;
374439 }
375- lineString += line . translateToString ( ! lineWrapsToNext && trimRight ) . substring ( 0 , terminal . cols ) ;
376440 lineIndex ++ ;
377- } while ( lineWrapsToNext ) ;
378-
379- return lineString ;
441+ line = nextLine ;
442+ }
443+ return [ strings . join ( '' ) , lineOffsets ] ;
380444 }
381445
382446 /**
@@ -390,7 +454,7 @@ export class SearchAddon implements ITerminalAddon {
390454 terminal . clearSelection ( ) ;
391455 return false ;
392456 }
393- terminal . select ( result . col , result . row , result . term . length ) ;
457+ terminal . select ( result . col , result . row , result . size ) ;
394458 // If it is not in the viewport then we scroll else it just gets selected
395459 if ( result . row >= ( terminal . buffer . active . viewportY + terminal . rows ) || result . row < terminal . buffer . active . viewportY ) {
396460 let scroll = result . row - terminal . buffer . active . viewportY ;
0 commit comments