@@ -64,6 +64,7 @@ const {
64
64
charLengthLeft,
65
65
commonPrefix,
66
66
kSubstringSearch,
67
+ reverseString,
67
68
} = require ( 'internal/readline/utils' ) ;
68
69
let emitKeypressEvents ;
69
70
let kFirstEventParam ;
@@ -98,9 +99,7 @@ const ESCAPE_CODE_TIMEOUT = 500;
98
99
// Max length of the kill ring
99
100
const kMaxLengthOfKillRing = 32 ;
100
101
101
- // TODO(puskin94): make this configurable
102
102
const kMultilinePrompt = Symbol ( '| ' ) ;
103
- const kLastCommandErrored = Symbol ( '_lastCommandErrored' ) ;
104
103
105
104
const kAddHistory = Symbol ( '_addHistory' ) ;
106
105
const kBeforeEdit = Symbol ( '_beforeEdit' ) ;
@@ -131,6 +130,7 @@ const kPrompt = Symbol('_prompt');
131
130
const kPushToKillRing = Symbol ( '_pushToKillRing' ) ;
132
131
const kPushToUndoStack = Symbol ( '_pushToUndoStack' ) ;
133
132
const kQuestionCallback = Symbol ( '_questionCallback' ) ;
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,10 +483,7 @@ 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 reverseString ( line , from , to ) ;
484
487
}
485
488
// For normal cases (saving to history or non-multiline entries)
486
489
return StringPrototypeReplaceAll ( line , from , to ) ;
@@ -494,22 +497,29 @@ class Interface extends InterfaceConstructor {
494
497
495
498
// If the trimmed line is empty then return the line
496
499
if ( StringPrototypeTrim ( this . line ) . length === 0 ) return this . line ;
497
- const normalizedLine = this [ kNormalizeHistoryLineEndings ] ( this . line , '\n' , '\r' , false ) ;
500
+
501
+ // This is necessary because each like would be saved in the history while creating
502
+ // A new multiline, and we don't want that.
503
+ if ( this [ kIsMultiline ] && this . historyIndex === - 1 ) {
504
+ ArrayPrototypeShift ( this . history ) ;
505
+ }
506
+ // If the last command errored and we are trying to edit the history to fix it
507
+ // Remove the broken one from the history
508
+ if ( this [ kLastCommandErrored ] && this . history . length > 0 ) {
509
+ ArrayPrototypeShift ( this . history ) ;
510
+ }
511
+
512
+ const normalizedLine = this [ kNormalizeHistoryLineEndings ] ( this . line , '\n' , '\r' , true ) ;
498
513
499
514
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
515
if ( this . removeHistoryDuplicates ) {
507
516
// Remove older history line if identical to new one
508
517
const dupIndex = ArrayPrototypeIndexOf ( this . history , this . line ) ;
509
518
if ( dupIndex !== - 1 ) ArrayPrototypeSplice ( this . history , dupIndex , 1 ) ;
510
519
}
511
520
512
- ArrayPrototypeUnshift ( this . history , this . line ) ;
521
+ // Add the new line to the history
522
+ ArrayPrototypeUnshift ( this . history , normalizedLine ) ;
513
523
514
524
// Only store so many
515
525
if ( this . history . length > this . historySize )
@@ -521,7 +531,7 @@ class Interface extends InterfaceConstructor {
521
531
// The listener could change the history object, possibly
522
532
// to remove the last added entry if it is sensitive and should
523
533
// not be persisted in the history, like a password
524
- const line = this . history [ 0 ] ;
534
+ const line = this [ kIsMultiline ] ? reverseString ( this . history [ 0 ] ) : this . history [ 0 ] ;
525
535
526
536
// Emit history event to notify listeners of update
527
537
this . emit ( 'history' , this . history ) ;
@@ -938,6 +948,18 @@ class Interface extends InterfaceConstructor {
938
948
}
939
949
}
940
950
951
+ [ kSavePreviousState ] ( ) {
952
+ this [ kPreviousLine ] = this . line ;
953
+ this [ kPreviousCursor ] = this . cursor ;
954
+ this [ kPreviousPrevRows ] = this . prevRows ;
955
+ }
956
+
957
+ [ kRestorePreviousState ] ( ) {
958
+ this [ kSetLine ] ( this [ kPreviousLine ] ) ;
959
+ this . cursor = this [ kPreviousCursor ] ;
960
+ this . prevRows = this [ kPreviousPrevRows ] ;
961
+ }
962
+
941
963
clearLine ( ) {
942
964
this [ kMoveCursor ] ( + Infinity ) ;
943
965
this [ kWriteToOutput ] ( '\r\n' ) ;
@@ -947,13 +969,117 @@ class Interface extends InterfaceConstructor {
947
969
}
948
970
949
971
[ kLine ] ( ) {
972
+ this [ kSavePreviousState ] ( ) ;
950
973
const line = this [ kAddHistory ] ( ) ;
951
974
this [ kUndoStack ] = [ ] ;
952
975
this [ kRedoStack ] = [ ] ;
953
976
this . clearLine ( ) ;
954
977
this [ kOnLine ] ( line ) ;
955
978
}
956
979
980
+
981
+ // TODO(puskin94): edit [kTtyWrite] to make call this function on a new key combination
982
+ // to make it add a new line in the middle of a "complete" multiline.
983
+ // I tried with shift + enter but it is not detected. Find a new one.
984
+ // Make sure to call this[kSavePreviousState](); && this.clearLine();
985
+ // before calling this[kAddNewLineOnTTY] to simulate what [kLine] is doing.
986
+
987
+ // When this function is called, the actual cursor is at the very end of the whole string,
988
+ // No matter where the new line was entered.
989
+ // This function should only be used when the output is a TTY
990
+ [ kAddNewLineOnTTY ] ( ) {
991
+ if ( ! this . terminal ) return ;
992
+
993
+ // Restore terminal state and store current line
994
+ this [ kRestorePreviousState ] ( ) ;
995
+ const originalLine = this . line ;
996
+
997
+ // Split the line at the current cursor position
998
+ const beforeCursor = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
999
+ let afterCursor = StringPrototypeSlice ( this . line , this . cursor , this . line . length ) ;
1000
+
1001
+ // Add the new line where the cursor is at
1002
+ this [ kSetLine ] ( `${ beforeCursor } \n${ afterCursor } ` ) ;
1003
+
1004
+ // To account for the new line
1005
+ this . cursor += 1 ;
1006
+
1007
+ const hasContentAfterCursor = afterCursor . length > 0 ;
1008
+ const cursorIsNotOnFirstLine = this . prevRows > 0 ;
1009
+ let needsRewriteFirstLine = false ;
1010
+
1011
+ // Handle cursor positioning based on different scenarios
1012
+ if ( hasContentAfterCursor ) {
1013
+ const splitBeg = StringPrototypeSplit ( beforeCursor , '\n' ) ;
1014
+ // Determine if we need to rewrite the first line
1015
+ needsRewriteFirstLine = splitBeg . length < 2 ;
1016
+
1017
+ // If the cursor is not on the first line
1018
+ if ( cursorIsNotOnFirstLine ) {
1019
+ const splitEnd = StringPrototypeSplit ( afterCursor , '\n' ) ;
1020
+
1021
+ // If the cursor when I pressed enter was at least on the second line
1022
+ // I need to completely erase the line where the cursor was pressed because it is possible
1023
+ // That it was pressed in the middle of the line, hence I need to write the whole line.
1024
+ // To achieve that, I need to reach the line above the current line coming from the end
1025
+ const dy = splitEnd . length + 1 ;
1026
+
1027
+ // Calculate how many Xs we need to move on the right to get to the end of the line
1028
+ const dxEndOfLineAbove = ( splitBeg [ splitBeg . length - 2 ] || '' ) . length + kMultilinePrompt . description . length ;
1029
+ moveCursor ( this . output , dxEndOfLineAbove , - dy ) ;
1030
+
1031
+ // This is the line that was split in the middle
1032
+ // Just add it to the rest of the line that will be printed later
1033
+ afterCursor = `${ splitBeg [ splitBeg . length - 1 ] } \n${ afterCursor } ` ;
1034
+ } else {
1035
+ // Otherwise, go to the very beginning of the first line and erase everything
1036
+ const dy = StringPrototypeSplit ( originalLine , '\n' ) . length ;
1037
+ moveCursor ( this . output , 0 , - dy ) ;
1038
+ }
1039
+
1040
+ // Erase from the cursor to the end of the line
1041
+ clearScreenDown ( this . output ) ;
1042
+
1043
+ if ( cursorIsNotOnFirstLine ) {
1044
+ this [ kWriteToOutput ] ( '\n' ) ;
1045
+ }
1046
+ }
1047
+
1048
+ if ( needsRewriteFirstLine ) {
1049
+ this [ kWriteToOutput ] ( `${ this [ kPrompt ] } ${ beforeCursor } \n${ kMultilinePrompt . description } ` ) ;
1050
+ } else {
1051
+ this [ kWriteToOutput ] ( kMultilinePrompt . description ) ;
1052
+ }
1053
+
1054
+ // Write the rest and restore the cursor to where the user left it
1055
+ if ( hasContentAfterCursor ) {
1056
+ // Save the cursor pos, we need to come back here
1057
+ const oldCursor = this . getCursorPos ( ) ;
1058
+
1059
+ // Write everything after the cursor which has been deleted by clearScreenDown
1060
+ const formattedEndContent = StringPrototypeReplaceAll (
1061
+ afterCursor ,
1062
+ '\n' ,
1063
+ `\n${ kMultilinePrompt . description } ` ,
1064
+ ) ;
1065
+
1066
+ this [ kWriteToOutput ] ( formattedEndContent ) ;
1067
+
1068
+ const newCursor = this [ kGetDisplayPos ] ( this . line ) ;
1069
+
1070
+ // Go back to where the cursor was, with relative movement
1071
+ moveCursor ( this . output , oldCursor . cols - newCursor . cols , oldCursor . rows - newCursor . rows ) ;
1072
+
1073
+ // Setting how many rows we have on top of the cursor
1074
+ // Necessary for kRefreshLine
1075
+ this . prevRows = oldCursor . rows ;
1076
+ } else {
1077
+ // Setting how many rows we have on top of the cursor
1078
+ // Necessary for kRefreshLine
1079
+ this . prevRows = StringPrototypeSplit ( this . line , '\n' ) . length - 1 ;
1080
+ }
1081
+ }
1082
+
957
1083
[ kPushToUndoStack ] ( text , cursor ) {
958
1084
if ( ArrayPrototypePush ( this [ kUndoStack ] , { text, cursor } ) >
959
1085
kMaxUndoRedoStackSize ) {
@@ -1525,6 +1651,7 @@ module.exports = {
1525
1651
kWordRight,
1526
1652
kWriteToOutput,
1527
1653
kMultilinePrompt,
1654
+ kRestorePreviousState,
1655
+ kAddNewLineOnTTY,
1528
1656
kLastCommandErrored,
1529
- kNormalizeHistoryLineEndings,
1530
1657
} ;
0 commit comments