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