@@ -64,6 +64,7 @@ const {
6464 charLengthLeft,
6565 commonPrefix,
6666 kSubstringSearch,
67+ reverseString,
6768} = require ( 'internal/readline/utils' ) ;
6869let emitKeypressEvents ;
6970let kFirstEventParam ;
@@ -98,9 +99,7 @@ const ESCAPE_CODE_TIMEOUT = 500;
9899// Max length of the kill ring
99100const kMaxLengthOfKillRing = 32 ;
100101
101- // TODO(puskin94): make this configurable
102102const kMultilinePrompt = Symbol ( '| ' ) ;
103- const kLastCommandErrored = Symbol ( '_lastCommandErrored' ) ;
104103
105104const kAddHistory = Symbol ( '_addHistory' ) ;
106105const kBeforeEdit = Symbol ( '_beforeEdit' ) ;
@@ -131,6 +130,7 @@ const kPrompt = Symbol('_prompt');
131130const kPushToKillRing = Symbol ( '_pushToKillRing' ) ;
132131const kPushToUndoStack = Symbol ( '_pushToUndoStack' ) ;
133132const kQuestionCallback = Symbol ( '_questionCallback' ) ;
133+ const kLastCommandErrored = Symbol ( '_lastCommandErrored' ) ;
134134const kQuestionReject = Symbol ( '_questionReject' ) ;
135135const kRedo = Symbol ( '_redo' ) ;
136136const kRedoStack = Symbol ( '_redoStack' ) ;
@@ -151,6 +151,12 @@ const kYank = Symbol('_yank');
151151const kYanking = Symbol ( '_yanking' ) ;
152152const kYankPop = Symbol ( '_yankPop' ) ;
153153const 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' ) ;
154160
155161function InterfaceConstructor ( input , output , completer , terminal ) {
156162 this [ kSawReturnAt ] = 0 ;
@@ -430,7 +436,7 @@ class Interface extends InterfaceConstructor {
430436 }
431437 }
432438
433- [ kSetLine ] ( line ) {
439+ [ kSetLine ] ( line = '' ) {
434440 this . line = line ;
435441 this [ kIsMultiline ] = StringPrototypeIncludes ( line , '\n' ) ;
436442 }
@@ -477,10 +483,7 @@ class Interface extends InterfaceConstructor {
477483 // Reversing the multilines is necessary when adding / editing and displaying them
478484 if ( reverse ) {
479485 // 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 ) ;
484487 }
485488 // For normal cases (saving to history or non-multiline entries)
486489 return StringPrototypeReplaceAll ( line , from , to ) ;
@@ -494,22 +497,29 @@ class Interface extends InterfaceConstructor {
494497
495498 // If the trimmed line is empty then return the line
496499 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+ }
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 ] ) {
509+ ArrayPrototypeShift ( this . history ) ;
510+ }
511+
512+ const normalizedLine = this [ kNormalizeHistoryLineEndings ] ( this . line , '\n' , '\r' , true ) ;
498513
499514 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- }
506515 if ( this . removeHistoryDuplicates ) {
507516 // Remove older history line if identical to new one
508517 const dupIndex = ArrayPrototypeIndexOf ( this . history , this . line ) ;
509518 if ( dupIndex !== - 1 ) ArrayPrototypeSplice ( this . history , dupIndex , 1 ) ;
510519 }
511520
512- ArrayPrototypeUnshift ( this . history , this . line ) ;
521+ // Add the new line to the history
522+ ArrayPrototypeUnshift ( this . history , normalizedLine ) ;
513523
514524 // Only store so many
515525 if ( this . history . length > this . historySize )
@@ -521,7 +531,7 @@ class Interface extends InterfaceConstructor {
521531 // The listener could change the history object, possibly
522532 // to remove the last added entry if it is sensitive and should
523533 // 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 ] ;
525535
526536 // Emit history event to notify listeners of update
527537 this . emit ( 'history' , this . history ) ;
@@ -938,6 +948,18 @@ class Interface extends InterfaceConstructor {
938948 }
939949 }
940950
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+
941963 clearLine ( ) {
942964 this [ kMoveCursor ] ( + Infinity ) ;
943965 this [ kWriteToOutput ] ( '\r\n' ) ;
@@ -947,13 +969,115 @@ class Interface extends InterfaceConstructor {
947969 }
948970
949971 [ kLine ] ( ) {
972+ this [ kSavePreviousState ] ( ) ;
950973 const line = this [ kAddHistory ] ( ) ;
951974 this [ kUndoStack ] = [ ] ;
952975 this [ kRedoStack ] = [ ] ;
953976 this . clearLine ( ) ;
954977 this [ kOnLine ] ( line ) ;
955978 }
956979
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+ // Restore terminal state and store current line
992+ this [ kRestorePreviousState ] ( ) ;
993+ const originalLine = this . line ;
994+
995+ // Split the line at the current cursor position
996+ const beforeCursor = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
997+ let afterCursor = StringPrototypeSlice ( this . line , this . cursor , this . line . length ) ;
998+
999+ // Add the new line where the cursor is at
1000+ this [ kSetLine ] ( `${ beforeCursor } \n${ afterCursor } ` ) ;
1001+
1002+ // To account for the new line
1003+ this . cursor += 1 ;
1004+
1005+ const hasContentAfterCursor = afterCursor . length > 0 ;
1006+ const cursorIsNotOnFirstLine = this . prevRows > 0 ;
1007+ let needsRewriteFirstLine = false ;
1008+
1009+ // Handle cursor positioning based on different scenarios
1010+ if ( hasContentAfterCursor ) {
1011+ const splitBeg = StringPrototypeSplit ( beforeCursor , '\n' ) ;
1012+ // Determine if we need to rewrite the first line
1013+ needsRewriteFirstLine = splitBeg . length < 2 ;
1014+
1015+ // If the cursor is not on the first line
1016+ if ( cursorIsNotOnFirstLine ) {
1017+ const splitEnd = StringPrototypeSplit ( afterCursor , '\n' ) ;
1018+
1019+ // If the cursor when I pressed enter was at least on the second line
1020+ // I need to completely erase the line where the cursor was pressed because it is possible
1021+ // That it was pressed in the middle of the line, hence I need to write the whole line.
1022+ // To achieve that, I need to reach the line above the current line coming from the end
1023+ const dy = splitEnd . length + 1 ;
1024+
1025+ // Calculate how many Xs we need to move on the right to get to the end of the line
1026+ const dxEndOfLineAbove = ( splitBeg [ splitBeg . length - 2 ] || '' ) . length + kMultilinePrompt . description . length ;
1027+ moveCursor ( this . output , dxEndOfLineAbove , - dy ) ;
1028+
1029+ // This is the line that was split in the middle
1030+ // Just add it to the rest of the line that will be printed later
1031+ afterCursor = `${ splitBeg [ splitBeg . length - 1 ] } \n${ afterCursor } ` ;
1032+ } else {
1033+ // Otherwise, go to the very beginning of the first line and erase everything
1034+ const dy = StringPrototypeSplit ( originalLine , '\n' ) . length ;
1035+ moveCursor ( this . output , 0 , - dy ) ;
1036+ }
1037+
1038+ // Erase from the cursor to the end of the line
1039+ clearScreenDown ( this . output ) ;
1040+
1041+ if ( cursorIsNotOnFirstLine ) {
1042+ this [ kWriteToOutput ] ( '\n' ) ;
1043+ }
1044+ }
1045+
1046+ if ( needsRewriteFirstLine ) {
1047+ this [ kWriteToOutput ] ( `${ this [ kPrompt ] } ${ beforeCursor } \n${ kMultilinePrompt . description } ` ) ;
1048+ } else {
1049+ this [ kWriteToOutput ] ( kMultilinePrompt . description ) ;
1050+ }
1051+
1052+ // Write the rest and restore the cursor to where the user left it
1053+ if ( hasContentAfterCursor ) {
1054+ // Save the cursor pos, we need to come back here
1055+ const oldCursor = this . getCursorPos ( ) ;
1056+
1057+ // Write everything after the cursor which has been deleted by clearScreenDown
1058+ const formattedEndContent = StringPrototypeReplaceAll (
1059+ afterCursor ,
1060+ '\n' ,
1061+ `\n${ kMultilinePrompt . description } ` ,
1062+ ) ;
1063+
1064+ this [ kWriteToOutput ] ( formattedEndContent ) ;
1065+
1066+ const newCursor = this [ kGetDisplayPos ] ( this . line ) ;
1067+
1068+ // Go back to where the cursor was, with relative movement
1069+ moveCursor ( this . output , oldCursor . cols - newCursor . cols , oldCursor . rows - newCursor . rows ) ;
1070+
1071+ // Setting how many rows we have on top of the cursor
1072+ // Necessary for kRefreshLine
1073+ this . prevRows = oldCursor . rows ;
1074+ } else {
1075+ // Setting how many rows we have on top of the cursor
1076+ // Necessary for kRefreshLine
1077+ this . prevRows = StringPrototypeSplit ( this . line , '\n' ) . length - 1 ;
1078+ }
1079+ }
1080+
9571081 [ kPushToUndoStack ] ( text , cursor ) {
9581082 if ( ArrayPrototypePush ( this [ kUndoStack ] , { text, cursor } ) >
9591083 kMaxUndoRedoStackSize ) {
@@ -1525,6 +1649,7 @@ module.exports = {
15251649 kWordRight,
15261650 kWriteToOutput,
15271651 kMultilinePrompt,
1652+ kRestorePreviousState,
1653+ kAddNewLineOnTTY,
15281654 kLastCommandErrored,
1529- kNormalizeHistoryLineEndings,
15301655} ;
0 commit comments