@@ -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,28 @@ 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+ } 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 ) ;
498512
499513 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- }
506514 if ( this . removeHistoryDuplicates ) {
507515 // Remove older history line if identical to new one
508516 const dupIndex = ArrayPrototypeIndexOf ( this . history , this . line ) ;
509517 if ( dupIndex !== - 1 ) ArrayPrototypeSplice ( this . history , dupIndex , 1 ) ;
510518 }
511519
512- ArrayPrototypeUnshift ( this . history , this . line ) ;
520+ // Add the new line to the history
521+ ArrayPrototypeUnshift ( this . history , normalizedLine ) ;
513522
514523 // Only store so many
515524 if ( this . history . length > this . historySize )
@@ -521,7 +530,7 @@ class Interface extends InterfaceConstructor {
521530 // The listener could change the history object, possibly
522531 // to remove the last added entry if it is sensitive and should
523532 // 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 ] ;
525534
526535 // Emit history event to notify listeners of update
527536 this . emit ( 'history' , this . history ) ;
@@ -938,6 +947,18 @@ class Interface extends InterfaceConstructor {
938947 }
939948 }
940949
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+
941962 clearLine ( ) {
942963 this [ kMoveCursor ] ( + Infinity ) ;
943964 this [ kWriteToOutput ] ( '\r\n' ) ;
@@ -947,13 +968,115 @@ class Interface extends InterfaceConstructor {
947968 }
948969
949970 [ kLine ] ( ) {
971+ this [ kSavePreviousState ] ( ) ;
950972 const line = this [ kAddHistory ] ( ) ;
951973 this [ kUndoStack ] = [ ] ;
952974 this [ kRedoStack ] = [ ] ;
953975 this . clearLine ( ) ;
954976 this [ kOnLine ] ( line ) ;
955977 }
956978
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+
9571080 [ kPushToUndoStack ] ( text , cursor ) {
9581081 if ( ArrayPrototypePush ( this [ kUndoStack ] , { text, cursor } ) >
9591082 kMaxUndoRedoStackSize ) {
@@ -1525,6 +1648,7 @@ module.exports = {
15251648 kWordRight,
15261649 kWriteToOutput,
15271650 kMultilinePrompt,
1651+ kRestorePreviousState,
1652+ kAddNewLineOnTTY,
15281653 kLastCommandErrored,
1529- kNormalizeHistoryLineEndings,
15301654} ;
0 commit comments