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