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