@@ -98,9 +98,7 @@ const ESCAPE_CODE_TIMEOUT = 500;
98
98
// Max length of the kill ring
99
99
const kMaxLengthOfKillRing = 32 ;
100
100
101
- // TODO(puskin94): make this configurable
102
101
const kMultilinePrompt = Symbol ( '| ' ) ;
103
- const kLastCommandErrored = Symbol ( '_lastCommandErrored' ) ;
104
102
105
103
const kAddHistory = Symbol ( '_addHistory' ) ;
106
104
const kBeforeEdit = Symbol ( '_beforeEdit' ) ;
@@ -131,6 +129,8 @@ const kPrompt = Symbol('_prompt');
131
129
const kPushToKillRing = Symbol ( '_pushToKillRing' ) ;
132
130
const kPushToUndoStack = Symbol ( '_pushToUndoStack' ) ;
133
131
const kQuestionCallback = Symbol ( '_questionCallback' ) ;
132
+ const kReverseString = Symbol ( '_reverseString' ) ;
133
+ const kLastCommandErrored = Symbol ( '_lastCommandErrored' ) ;
134
134
const kQuestionReject = Symbol ( '_questionReject' ) ;
135
135
const kRedo = Symbol ( '_redo' ) ;
136
136
const kRedoStack = Symbol ( '_redoStack' ) ;
@@ -151,6 +151,12 @@ const kYank = Symbol('_yank');
151
151
const kYanking = Symbol ( '_yanking' ) ;
152
152
const kYankPop = Symbol ( '_yankPop' ) ;
153
153
const kNormalizeHistoryLineEndings = Symbol ( '_normalizeHistoryLineEndings' ) ;
154
+ const kSavePreviousState = Symbol ( '_savePreviousState' ) ;
155
+ const kRestorePreviousState = Symbol ( '_restorePreviousState' ) ;
156
+ const kPreviousLine = Symbol ( '_previousLine' ) ;
157
+ const kPreviousCursor = Symbol ( '_previousCursor' ) ;
158
+ const kPreviousPrevRows = Symbol ( '_previousPrevRows' ) ;
159
+ const kAddNewLineOnTTY = Symbol ( '_addNewLineOnTTY' ) ;
154
160
155
161
function InterfaceConstructor ( input , output , completer , terminal ) {
156
162
this [ kSawReturnAt ] = 0 ;
@@ -430,7 +436,7 @@ class Interface extends InterfaceConstructor {
430
436
}
431
437
}
432
438
433
- [ kSetLine ] ( line ) {
439
+ [ kSetLine ] ( line = '' ) {
434
440
this . line = line ;
435
441
this [ kIsMultiline ] = StringPrototypeIncludes ( line , '\n' ) ;
436
442
}
@@ -477,15 +483,26 @@ class Interface extends InterfaceConstructor {
477
483
// Reversing the multilines is necessary when adding / editing and displaying them
478
484
if ( reverse ) {
479
485
// First reverse the lines for proper order, then convert separators
480
- return ArrayPrototypeJoin (
481
- ArrayPrototypeReverse ( StringPrototypeSplit ( line , from ) ) ,
482
- to ,
483
- ) ;
486
+ return this [ kReverseString ] ( line , from , to ) ;
484
487
}
485
488
// For normal cases (saving to history or non-multiline entries)
486
489
return StringPrototypeReplaceAll ( line , from , to ) ;
487
490
}
488
491
492
+ [ kReverseString ] ( line , from , to ) {
493
+ const parts = StringPrototypeSplit ( line , from ) ;
494
+
495
+ // This implementation should be faster than
496
+ // ArrayPrototypeJoin(ArrayPrototypeReverse(StringPrototypeSplit(line, from)), to);
497
+ let result = '' ;
498
+ for ( let i = parts . length - 1 ; i > 0 ; i -- ) {
499
+ result += parts [ i ] + to ;
500
+ }
501
+ result += parts [ 0 ] ;
502
+
503
+ return result ;
504
+ }
505
+
489
506
[ kAddHistory ] ( ) {
490
507
if ( this . line . length === 0 ) return '' ;
491
508
@@ -494,22 +511,28 @@ class Interface extends InterfaceConstructor {
494
511
495
512
// If the trimmed line is empty then return the line
496
513
if ( StringPrototypeTrim ( this . line ) . length === 0 ) return this . line ;
497
- const normalizedLine = this [ kNormalizeHistoryLineEndings ] ( this . line , '\n' , '\r' , false ) ;
514
+
515
+ // This is necessary because each like would be saved in the history while creating
516
+ // A new multiline, and we don't want that.
517
+ if ( this [ kIsMultiline ] && this . historyIndex === - 1 ) {
518
+ ArrayPrototypeShift ( this . history ) ;
519
+ }
520
+ // If the last command errored and we are trying to edit the history to fix it
521
+ // Remove the broken one from the history
522
+ if ( this [ kLastCommandErrored ] && this . history . length > 0 ) {
523
+ ArrayPrototypeShift ( this . history ) ;
524
+ }
525
+
526
+ const normalizedLine = this [ kNormalizeHistoryLineEndings ] ( this . line , '\n' , '\r' , true ) ;
498
527
499
528
if ( this . history . length === 0 || this . history [ 0 ] !== normalizedLine ) {
500
- if ( this [ kLastCommandErrored ] && this . historyIndex === 0 ) {
501
- // If the last command errored, remove it from history.
502
- // The user is issuing a new command starting from the errored command,
503
- // Hopefully with the fix
504
- ArrayPrototypeShift ( this . history ) ;
505
- }
506
529
if ( this . removeHistoryDuplicates ) {
507
530
// Remove older history line if identical to new one
508
531
const dupIndex = ArrayPrototypeIndexOf ( this . history , this . line ) ;
509
532
if ( dupIndex !== - 1 ) ArrayPrototypeSplice ( this . history , dupIndex , 1 ) ;
510
533
}
511
534
512
- ArrayPrototypeUnshift ( this . history , this . line ) ;
535
+ ArrayPrototypeUnshift ( this . history , normalizedLine ) ;
513
536
514
537
// Only store so many
515
538
if ( this . history . length > this . historySize )
@@ -521,7 +544,7 @@ class Interface extends InterfaceConstructor {
521
544
// The listener could change the history object, possibly
522
545
// to remove the last added entry if it is sensitive and should
523
546
// not be persisted in the history, like a password
524
- const line = this . history [ 0 ] ;
547
+ const line = this [ kIsMultiline ] ? this [ kReverseString ] ( this . history [ 0 ] , '\r' , '\r' ) : this . history [ 0 ] ;
525
548
526
549
// Emit history event to notify listeners of update
527
550
this . emit ( 'history' , this . history ) ;
@@ -938,6 +961,18 @@ class Interface extends InterfaceConstructor {
938
961
}
939
962
}
940
963
964
+ [ kSavePreviousState ] ( ) {
965
+ this [ kPreviousLine ] = this . line ;
966
+ this [ kPreviousCursor ] = this . cursor ;
967
+ this [ kPreviousPrevRows ] = this . prevRows ;
968
+ }
969
+
970
+ [ kRestorePreviousState ] ( ) {
971
+ this [ kSetLine ] ( this [ kPreviousLine ] ) ;
972
+ this . cursor = this [ kPreviousCursor ] ;
973
+ this . prevRows = this [ kPreviousPrevRows ] ;
974
+ }
975
+
941
976
clearLine ( ) {
942
977
this [ kMoveCursor ] ( + Infinity ) ;
943
978
this [ kWriteToOutput ] ( '\r\n' ) ;
@@ -947,13 +982,117 @@ class Interface extends InterfaceConstructor {
947
982
}
948
983
949
984
[ kLine ] ( ) {
985
+ this [ kSavePreviousState ] ( ) ;
950
986
const line = this [ kAddHistory ] ( ) ;
951
987
this [ kUndoStack ] = [ ] ;
952
988
this [ kRedoStack ] = [ ] ;
953
989
this . clearLine ( ) ;
954
990
this [ kOnLine ] ( line ) ;
955
991
}
956
992
993
+
994
+ // TODO(puskin94): edit [kTtyWrite] to make call this function on a new key combination
995
+ // to make it add a new line in the middle of a "complete" multiline.
996
+ // I tried with shift + enter but it is not detected. Find a new one.
997
+ // Make sure to call this[kSavePreviousState](); && this.clearLine();
998
+ // before calling this[kAddNewLineOnTTY] to simulate what [kLine] is doing.
999
+
1000
+ // When this function is called, the actual cursor is at the very end of the whole string,
1001
+ // No matter where the new line was entered.
1002
+ // This function should only be used when the output is a TTY
1003
+ [ kAddNewLineOnTTY ] ( ) {
1004
+ if ( ! this . terminal ) return ;
1005
+
1006
+ // Restore terminal state and store current line
1007
+ this [ kRestorePreviousState ] ( ) ;
1008
+ const originalLine = this . line ;
1009
+
1010
+ // Split the line at the current cursor position
1011
+ const beforeCursor = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
1012
+ let afterCursor = StringPrototypeSlice ( this . line , this . cursor , this . line . length ) ;
1013
+
1014
+ // Add the new line where the cursor is at
1015
+ this [ kSetLine ] ( `${ beforeCursor } \n${ afterCursor } ` ) ;
1016
+
1017
+ // To account for the new line
1018
+ this . cursor += 1 ;
1019
+
1020
+ const hasContentAfterCursor = afterCursor . length > 0 ;
1021
+ const cursorIsNotOnFirstLine = this . prevRows > 0 ;
1022
+ let needsRewriteFirstLine = false ;
1023
+
1024
+ // Handle cursor positioning based on different scenarios
1025
+ if ( hasContentAfterCursor ) {
1026
+ const splitBeg = StringPrototypeSplit ( beforeCursor , '\n' ) ;
1027
+ // Determine if we need to rewrite the first line
1028
+ needsRewriteFirstLine = splitBeg . length < 2 ;
1029
+
1030
+ // If the cursor is not on the first line
1031
+ if ( cursorIsNotOnFirstLine ) {
1032
+ const splitEnd = StringPrototypeSplit ( afterCursor , '\n' ) ;
1033
+
1034
+ // If the cursor when I pressed enter was at least on the second line
1035
+ // I need to completely erase the line where the cursor was pressed because it is possible
1036
+ // That it was pressed in the middle of the line, hence I need to write the whole line.
1037
+ // To achieve that, I need to reach the line above the current line coming from the end
1038
+ const dy = splitEnd . length + 1 ;
1039
+
1040
+ // Calculate how many Xs we need to move on the right to get to the end of the line
1041
+ const dxEndOfLineAbove = ( splitBeg [ splitBeg . length - 2 ] || '' ) . length + kMultilinePrompt . description . length ;
1042
+ moveCursor ( this . output , dxEndOfLineAbove , - dy ) ;
1043
+
1044
+ // This is the line that was split in the middle
1045
+ // Just add it to the rest of the line that will be printed later
1046
+ afterCursor = `${ splitBeg [ splitBeg . length - 1 ] } \n${ afterCursor } ` ;
1047
+ } else {
1048
+ // Otherwise, go to the very beginning of the first line and erase everything
1049
+ const dy = StringPrototypeSplit ( originalLine , '\n' ) . length ;
1050
+ moveCursor ( this . output , 0 , - dy ) ;
1051
+ }
1052
+
1053
+ // Erase from the cursor to the end of the line
1054
+ clearScreenDown ( this . output ) ;
1055
+
1056
+ if ( cursorIsNotOnFirstLine ) {
1057
+ this [ kWriteToOutput ] ( '\n' ) ;
1058
+ }
1059
+ }
1060
+
1061
+ if ( needsRewriteFirstLine ) {
1062
+ this [ kWriteToOutput ] ( `${ this [ kPrompt ] } ${ beforeCursor } \n${ kMultilinePrompt . description } ` ) ;
1063
+ } else {
1064
+ this [ kWriteToOutput ] ( kMultilinePrompt . description ) ;
1065
+ }
1066
+
1067
+ // Write the rest and restore the cursor to where the user left it
1068
+ if ( hasContentAfterCursor ) {
1069
+ // Save the cursor pos, we need to come back here
1070
+ const oldCursor = this . getCursorPos ( ) ;
1071
+
1072
+ // Write everything after the cursor which has been deleted by clearScreenDown
1073
+ const formattedEndContent = StringPrototypeReplaceAll (
1074
+ afterCursor ,
1075
+ '\n' ,
1076
+ `\n${ kMultilinePrompt . description } ` ,
1077
+ ) ;
1078
+
1079
+ this [ kWriteToOutput ] ( formattedEndContent ) ;
1080
+
1081
+ const newCursor = this [ kGetDisplayPos ] ( this . line ) ;
1082
+
1083
+ // Go back to where the cursor was, with relative movement
1084
+ moveCursor ( this . output , oldCursor . cols - newCursor . cols , oldCursor . rows - newCursor . rows ) ;
1085
+
1086
+ // Setting how many rows we have on top of the cursor
1087
+ // Necessary for kRefreshLine
1088
+ this . prevRows = oldCursor . rows ;
1089
+ } else {
1090
+ // Setting how many rows we have on top of the cursor
1091
+ // Necessary for kRefreshLine
1092
+ this . prevRows = StringPrototypeSplit ( this . line , '\n' ) . length - 1 ;
1093
+ }
1094
+ }
1095
+
957
1096
[ kPushToUndoStack ] ( text , cursor ) {
958
1097
if ( ArrayPrototypePush ( this [ kUndoStack ] , { text, cursor } ) >
959
1098
kMaxUndoRedoStackSize ) {
@@ -1525,6 +1664,7 @@ module.exports = {
1525
1664
kWordRight,
1526
1665
kWriteToOutput,
1527
1666
kMultilinePrompt,
1667
+ kRestorePreviousState,
1668
+ kAddNewLineOnTTY,
1528
1669
kLastCommandErrored,
1529
- kNormalizeHistoryLineEndings,
1530
1670
} ;
0 commit comments