@@ -24,6 +24,7 @@ const {
24
24
25
25
const {
26
26
clearLine,
27
+ clearScreenDown,
27
28
cursorTo,
28
29
moveCursor,
29
30
} = require ( 'readline' ) ;
@@ -42,7 +43,13 @@ const inspectOptions = {
42
43
compact : true ,
43
44
breakLength : Infinity
44
45
} ;
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
+ } ) ;
46
53
47
54
// If the error is that we've unexpectedly ended the input,
48
55
// then let the user try to recover by adding more input.
@@ -393,8 +400,242 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
393
400
return { showPreview, clearPreview } ;
394
401
}
395
402
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
+
396
636
module . exports = {
397
637
isRecoverableError,
398
638
kStandaloneREPL : Symbol ( 'kStandaloneREPL' ) ,
399
- setupPreview
639
+ setupPreview,
640
+ setupReverseSearch
400
641
} ;
0 commit comments