22
33const {
44 MathMin,
5+ Set,
56 Symbol,
67} = primordials ;
78
@@ -24,6 +25,7 @@ const {
2425
2526const {
2627 clearLine,
28+ clearScreenDown,
2729 cursorTo,
2830 moveCursor,
2931} = require ( 'readline' ) ;
@@ -42,7 +44,13 @@ const inspectOptions = {
4244 compact : true ,
4345 breakLength : Infinity
4446} ;
45- const inspectedOptions = inspect ( inspectOptions , { colors : false } ) ;
47+ // Specify options that might change the output in a way that it's not a valid
48+ // stringified object anymore.
49+ const inspectedOptions = inspect ( inspectOptions , {
50+ depth : 1 ,
51+ colors : false ,
52+ showHidden : false
53+ } ) ;
4654
4755// If the error is that we've unexpectedly ended the input,
4856// then let the user try to recover by adding more input.
@@ -393,8 +401,242 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
393401 return { showPreview, clearPreview } ;
394402}
395403
404+ function setupReverseSearch ( repl ) {
405+ // Simple terminals can't use reverse search.
406+ if ( process . env . TERM === 'dumb' ) {
407+ return { reverseSearch ( ) { return false ; } } ;
408+ }
409+
410+ const alreadyMatched = new Set ( ) ;
411+ const labels = {
412+ r : 'bck-i-search: ' ,
413+ s : 'fwd-i-search: '
414+ } ;
415+ let isInReverseSearch = false ;
416+ let historyIndex = - 1 ;
417+ let input = '' ;
418+ let cursor = - 1 ;
419+ let dir = 'r' ;
420+ let lastMatch = - 1 ;
421+ let lastCursor = - 1 ;
422+ let promptPos ;
423+
424+ function checkAndSetDirectionKey ( keyName ) {
425+ if ( ! labels [ keyName ] ) {
426+ return false ;
427+ }
428+ if ( dir !== keyName ) {
429+ // Reset the already matched set in case the direction is changed. That
430+ // way it's possible to find those entries again.
431+ alreadyMatched . clear ( ) ;
432+ }
433+ dir = keyName ;
434+ return true ;
435+ }
436+
437+ function goToNextHistoryIndex ( ) {
438+ // Ignore this entry for further searches and continue to the next
439+ // history entry.
440+ alreadyMatched . add ( repl . history [ historyIndex ] ) ;
441+ historyIndex += dir === 'r' ? 1 : - 1 ;
442+ cursor = - 1 ;
443+ }
444+
445+ function search ( ) {
446+ // Just print an empty line in case the user removed the search parameter.
447+ if ( input === '' ) {
448+ print ( repl . line , `${ labels [ dir ] } _` ) ;
449+ return ;
450+ }
451+ // Fix the bounds in case the direction has changed in the meanwhile.
452+ if ( dir === 'r' ) {
453+ if ( historyIndex < 0 ) {
454+ historyIndex = 0 ;
455+ }
456+ } else if ( historyIndex >= repl . history . length ) {
457+ historyIndex = repl . history . length - 1 ;
458+ }
459+ // Check the history entries until a match is found.
460+ while ( historyIndex >= 0 && historyIndex < repl . history . length ) {
461+ let entry = repl . history [ historyIndex ] ;
462+ // Visualize all potential matches only once.
463+ if ( alreadyMatched . has ( entry ) ) {
464+ historyIndex += dir === 'r' ? 1 : - 1 ;
465+ continue ;
466+ }
467+ // Match the next entry either from the start or from the end, depending
468+ // on the current direction.
469+ if ( dir === 'r' ) {
470+ // Update the cursor in case it's necessary.
471+ if ( cursor === - 1 ) {
472+ cursor = entry . length ;
473+ }
474+ cursor = entry . lastIndexOf ( input , cursor - 1 ) ;
475+ } else {
476+ cursor = entry . indexOf ( input , cursor + 1 ) ;
477+ }
478+ // Match not found.
479+ if ( cursor === - 1 ) {
480+ goToNextHistoryIndex ( ) ;
481+ // Match found.
482+ } else {
483+ if ( repl . useColors ) {
484+ const start = entry . slice ( 0 , cursor ) ;
485+ const end = entry . slice ( cursor + input . length ) ;
486+ entry = `${ start } \x1B[4m${ input } \x1B[24m${ end } ` ;
487+ }
488+ print ( entry , `${ labels [ dir ] } ${ input } _` , cursor ) ;
489+ lastMatch = historyIndex ;
490+ lastCursor = cursor ;
491+ // Explicitly go to the next history item in case no further matches are
492+ // possible with the current entry.
493+ if ( ( dir === 'r' && cursor === 0 ) ||
494+ ( dir === 's' && entry . length === cursor + input . length ) ) {
495+ goToNextHistoryIndex ( ) ;
496+ }
497+ return ;
498+ }
499+ }
500+ print ( repl . line , `failed-${ labels [ dir ] } ${ input } _` ) ;
501+ }
502+
503+ function print ( outputLine , inputLine , cursor = repl . cursor ) {
504+ // TODO(BridgeAR): Resizing the terminal window hides the overlay. To fix
505+ // that, readline must be aware of this information. It's probably best to
506+ // add a couple of properties to readline that allow to do the following:
507+ // 1. Add arbitrary data to the end of the current line while not counting
508+ // towards the line. This would be useful for the completion previews.
509+ // 2. Add arbitrary extra lines that do not count towards the regular line.
510+ // This would be useful for both, the input preview and the reverse
511+ // search. It might be combined with the first part?
512+ // 3. Add arbitrary input that is "on top" of the current line. That is
513+ // useful for the reverse search.
514+ // 4. To trigger the line refresh, functions should be used to pass through
515+ // the information. Alternatively, getters and setters could be used.
516+ // That might even be more elegant.
517+ // The data would then be accounted for when calling `_refreshLine()`.
518+ // This function would then look similar to:
519+ // repl.overlay(outputLine);
520+ // repl.addTrailingLine(inputLine);
521+ // repl.setCursor(cursor);
522+ // More potential improvements: use something similar to stream.cork().
523+ // Multiple cursor moves on the same tick could be prevented in case all
524+ // writes from the same tick are combined and the cursor is moved at the
525+ // tick end instead of after each operation.
526+ let rows = 0 ;
527+ if ( lastMatch !== - 1 ) {
528+ const line = repl . history [ lastMatch ] . slice ( 0 , lastCursor ) ;
529+ rows = repl . _getDisplayPos ( `${ repl . _prompt } ${ line } ` ) . rows ;
530+ cursorTo ( repl . output , promptPos . cols ) ;
531+ } else if ( isInReverseSearch && repl . line !== '' ) {
532+ rows = repl . _getCursorPos ( ) . rows ;
533+ cursorTo ( repl . output , promptPos . cols ) ;
534+ }
535+ if ( rows !== 0 )
536+ moveCursor ( repl . output , 0 , - rows ) ;
537+
538+ if ( isInReverseSearch ) {
539+ clearScreenDown ( repl . output ) ;
540+ repl . output . write ( `${ outputLine } \n${ inputLine } ` ) ;
541+ } else {
542+ repl . output . write ( `\n${ inputLine } ` ) ;
543+ }
544+
545+ lastMatch = - 1 ;
546+
547+ // To know exactly how many rows we have to move the cursor back we need the
548+ // cursor rows, the output rows and the input rows.
549+ const prompt = repl . _prompt ;
550+ const cursorLine = `${ prompt } ${ outputLine . slice ( 0 , cursor ) } ` ;
551+ const cursorPos = repl . _getDisplayPos ( cursorLine ) ;
552+ const outputPos = repl . _getDisplayPos ( `${ prompt } ${ outputLine } ` ) ;
553+ const inputPos = repl . _getDisplayPos ( inputLine ) ;
554+ const inputRows = inputPos . rows - ( inputPos . cols === 0 ? 1 : 0 ) ;
555+
556+ rows = - 1 - inputRows - ( outputPos . rows - cursorPos . rows ) ;
557+
558+ moveCursor ( repl . output , 0 , rows ) ;
559+ cursorTo ( repl . output , cursorPos . cols ) ;
560+ }
561+
562+ function reset ( string ) {
563+ isInReverseSearch = string !== undefined ;
564+
565+ // In case the reverse search ends and a history entry is found, reset the
566+ // line to the found entry.
567+ if ( ! isInReverseSearch ) {
568+ if ( lastMatch !== - 1 ) {
569+ repl . line = repl . history [ lastMatch ] ;
570+ repl . cursor = lastCursor ;
571+ repl . historyIndex = lastMatch ;
572+ }
573+
574+ lastMatch = - 1 ;
575+
576+ // Clear screen and write the current repl.line before exiting.
577+ cursorTo ( repl . output , promptPos . cols ) ;
578+ if ( promptPos . rows !== 0 )
579+ moveCursor ( repl . output , 0 , promptPos . rows ) ;
580+ clearScreenDown ( repl . output ) ;
581+ if ( repl . line !== '' ) {
582+ repl . output . write ( repl . line ) ;
583+ if ( repl . line . length !== repl . cursor ) {
584+ const { cols, rows } = repl . _getCursorPos ( ) ;
585+ cursorTo ( repl . output , cols ) ;
586+ if ( rows !== 0 )
587+ moveCursor ( repl . output , 0 , rows ) ;
588+ }
589+ }
590+ }
591+
592+ input = string || '' ;
593+ cursor = - 1 ;
594+ historyIndex = repl . historyIndex ;
595+ alreadyMatched . clear ( ) ;
596+ }
597+
598+ function reverseSearch ( string , key ) {
599+ if ( ! isInReverseSearch ) {
600+ if ( key . ctrl && checkAndSetDirectionKey ( key . name ) ) {
601+ historyIndex = repl . historyIndex ;
602+ promptPos = repl . _getDisplayPos ( `${ repl . _prompt } ` ) ;
603+ print ( repl . line , `${ labels [ dir ] } _` ) ;
604+ isInReverseSearch = true ;
605+ }
606+ } else if ( key . ctrl && checkAndSetDirectionKey ( key . name ) ) {
607+ search ( ) ;
608+ } else if ( key . name === 'backspace' ||
609+ ( key . ctrl && ( key . name === 'h' || key . name === 'w' ) ) ) {
610+ reset ( input . slice ( 0 , input . length - 1 ) ) ;
611+ search ( ) ;
612+ // Special handle <ctrl> + c and escape. Those should only cancel the
613+ // reverse search. The original line is visible afterwards again.
614+ } else if ( ( key . ctrl && key . name === 'c' ) || key . name === 'escape' ) {
615+ lastMatch = - 1 ;
616+ reset ( ) ;
617+ return true ;
618+ // End search in case either enter is pressed or if any non-reverse-search
619+ // key (combination) is pressed.
620+ } else if ( key . ctrl ||
621+ key . meta ||
622+ key . name === 'return' ||
623+ key . name === 'enter' ||
624+ typeof string !== 'string' ||
625+ string === '' ) {
626+ reset ( ) ;
627+ } else {
628+ reset ( `${ input } ${ string } ` ) ;
629+ search ( ) ;
630+ }
631+ return isInReverseSearch ;
632+ }
633+
634+ return { reverseSearch } ;
635+ }
636+
396637module . exports = {
397638 isRecoverableError,
398639 kStandaloneREPL : Symbol ( 'kStandaloneREPL' ) ,
399- setupPreview
640+ setupPreview,
641+ setupReverseSearch
400642} ;
0 commit comments