@@ -98,9 +98,7 @@ const ESCAPE_CODE_TIMEOUT = 500;
9898// Max length of the kill ring
9999const kMaxLengthOfKillRing = 32 ;
100100
101- // TODO(puskin94): make this configurable
102101const kMultilinePrompt = Symbol ( '| ' ) ;
103- const kLastCommandErrored = Symbol ( '_lastCommandErrored' ) ;
104102
105103const kAddHistory = Symbol ( '_addHistory' ) ;
106104const kBeforeEdit = Symbol ( '_beforeEdit' ) ;
@@ -131,6 +129,8 @@ const kPrompt = Symbol('_prompt');
131129const kPushToKillRing = Symbol ( '_pushToKillRing' ) ;
132130const kPushToUndoStack = Symbol ( '_pushToUndoStack' ) ;
133131const kQuestionCallback = Symbol ( '_questionCallback' ) ;
132+ const kReverseString = Symbol ( '_reverseString' ) ;
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,15 +483,26 @@ 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 this [ kReverseString ] ( line , from , to ) ;
484487 }
485488 // For normal cases (saving to history or non-multiline entries)
486489 return StringPrototypeReplaceAll ( line , from , to ) ;
487490 }
488491
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+
489506 [ kAddHistory ] ( ) {
490507 if ( this . line . length === 0 ) return '' ;
491508
@@ -494,22 +511,30 @@ class Interface extends InterfaceConstructor {
494511
495512 // If the trimmed line is empty then return the line
496513 if ( StringPrototypeTrim ( this . line ) . length === 0 ) return this . line ;
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+
497526 const normalizedLine = this [ kNormalizeHistoryLineEndings ] ( this . line , '\n' , '\r' , false ) ;
498527
499528 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- }
506529 if ( this . removeHistoryDuplicates ) {
507530 // Remove older history line if identical to new one
508531 const dupIndex = ArrayPrototypeIndexOf ( this . history , this . line ) ;
509532 if ( dupIndex !== - 1 ) ArrayPrototypeSplice ( this . history , dupIndex , 1 ) ;
510533 }
511534
512- ArrayPrototypeUnshift ( this . history , this . line ) ;
535+ ArrayPrototypeUnshift ( this . history , this [ kIsMultiline ] ?
536+ this [ kReverseString ] ( normalizedLine , '\r' , '\r' ) :
537+ normalizedLine ) ;
513538
514539 // Only store so many
515540 if ( this . history . length > this . historySize )
@@ -521,7 +546,7 @@ class Interface extends InterfaceConstructor {
521546 // The listener could change the history object, possibly
522547 // to remove the last added entry if it is sensitive and should
523548 // not be persisted in the history, like a password
524- const line = this . history [ 0 ] ;
549+ const line = this [ kIsMultiline ] ? this [ kReverseString ] ( this . history [ 0 ] , '\r' , '\r' ) : this . history [ 0 ] ;
525550
526551 // Emit history event to notify listeners of update
527552 this . emit ( 'history' , this . history ) ;
@@ -938,6 +963,18 @@ class Interface extends InterfaceConstructor {
938963 }
939964 }
940965
966+ [ kSavePreviousState ] ( ) {
967+ this [ kPreviousLine ] = this . line ;
968+ this [ kPreviousCursor ] = this . cursor ;
969+ this [ kPreviousPrevRows ] = this . prevRows ;
970+ }
971+
972+ [ kRestorePreviousState ] ( ) {
973+ this [ kSetLine ] ( this [ kPreviousLine ] ) ;
974+ this . cursor = this [ kPreviousCursor ] ;
975+ this . prevRows = this [ kPreviousPrevRows ] ;
976+ }
977+
941978 clearLine ( ) {
942979 this [ kMoveCursor ] ( + Infinity ) ;
943980 this [ kWriteToOutput ] ( '\r\n' ) ;
@@ -947,13 +984,117 @@ class Interface extends InterfaceConstructor {
947984 }
948985
949986 [ kLine ] ( ) {
987+ this [ kSavePreviousState ] ( ) ;
950988 const line = this [ kAddHistory ] ( ) ;
951989 this [ kUndoStack ] = [ ] ;
952990 this [ kRedoStack ] = [ ] ;
953991 this . clearLine ( ) ;
954992 this [ kOnLine ] ( line ) ;
955993 }
956994
995+
996+ // TODO(puskin94): edit [kTtyWrite] to make call this function on a new key combination
997+ // to make it add a new line in the middle of a "complete" multiline.
998+ // I tried with shift + enter but it is not detected. Find a new one.
999+ // Make sure to call this[kSavePreviousState](); && this.clearLine();
1000+ // before calling this[kAddNewLineOnTTY] to simulate what [kLine] is doing.
1001+
1002+ // When this function is called, the actual cursor is at the very end of the whole string,
1003+ // No matter where the new line was entered.
1004+ // This function should only be used when the output is a TTY
1005+ [ kAddNewLineOnTTY ] ( ) {
1006+ if ( ! this . terminal ) {
1007+ return ;
1008+ }
1009+
1010+ this [ kRestorePreviousState ] ( ) ;
1011+ const originalLine = this . line ;
1012+
1013+ // Before the cursor
1014+ const beg = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
1015+ // After the cursor
1016+ let end = StringPrototypeSlice ( this . line , this . cursor , this . line . length ) ;
1017+
1018+ // Add the new line where the cursor is at
1019+ this [ kSetLine ] ( `${ beg } \n${ end } ` ) ;
1020+
1021+ // To account for the new line
1022+ this . cursor += 1 ;
1023+
1024+ const hasContentAfterCursor = end . length > 0 ;
1025+ const cursorIsNotOnFirstLine = this . prevRows > 0 ;
1026+ const lineParts = hasContentAfterCursor ? StringPrototypeSplit ( beg , '\n' ) : null ;
1027+
1028+ // Handle cursor positioning based on different scenarios
1029+ if ( hasContentAfterCursor ) {
1030+ // If the cursor is not on the first line
1031+ if ( cursorIsNotOnFirstLine ) {
1032+ const splitEnd = StringPrototypeSplit ( end , '\n' ) ;
1033+ // If the cursor when I pressed enter was at least on the second line
1034+ // I need to completely erase the line where the cursor was pressed because it is possible
1035+ // That it was pressed in the middle of the line, hence I need to write the whole line.
1036+ // To achieve that, I need to reach the line above the current line coming from the end
1037+ const dy = splitEnd . length + 1 ;
1038+ // Calculate how many Xs we need to move on the right to get to the end of the line
1039+ const dxEndOfLineAbove = lineParts [ lineParts . length - 2 ] . length + kMultilinePrompt . description . length ;
1040+ moveCursor ( this . output , dxEndOfLineAbove , - dy ) ;
1041+ // This is the line that was split in the middle
1042+ // Just add it to the rest of the line that will be printed later
1043+ end = `${ lineParts [ lineParts . length - 1 ] } \n${ end } ` ;
1044+ } else {
1045+ // Otherwise, go to the very beginning of the first line and erase everything
1046+ const dy = StringPrototypeSplit ( originalLine , '\n' ) . length ;
1047+ moveCursor ( this . output , 0 , - dy ) ;
1048+ }
1049+ } else {
1050+ // Otherwise, go to the very beginning of the currentLine
1051+ // We need to rewrite everything
1052+ cursorTo ( this . output , 0 ) ;
1053+ }
1054+
1055+ // Erase from the cursor to the end of the line
1056+ clearScreenDown ( this . output ) ;
1057+
1058+ // Handle new line output for multiline case
1059+ if ( hasContentAfterCursor && cursorIsNotOnFirstLine ) {
1060+ this [ kWriteToOutput ] ( '\n' ) ;
1061+ }
1062+
1063+ // Determine if we need to rewrite the first line
1064+ const needsRewriteFirstLine = lineParts && lineParts . length < 2 ;
1065+ if ( needsRewriteFirstLine ) {
1066+ this [ kWriteToOutput ] ( `${ this [ kPrompt ] } ${ beg } \n${ kMultilinePrompt . description } ` ) ;
1067+ } else {
1068+ this [ kWriteToOutput ] ( kMultilinePrompt . description ) ;
1069+ }
1070+
1071+ // Write the rest and restore the cursor to where the user left it
1072+ if ( hasContentAfterCursor ) {
1073+ // Save the cursor pos, we need to come back here
1074+ const oldCursor = this . getCursorPos ( ) ;
1075+
1076+ // Write everything after the cursor which has been deleted by clearScreenDown
1077+ for ( const letter of end ) {
1078+ if ( letter === '\n' ) {
1079+ this [ kWriteToOutput ] ( `\n${ kMultilinePrompt . description } ` ) ;
1080+ } else {
1081+ this [ kWriteToOutput ] ( letter ) ;
1082+ }
1083+ }
1084+
1085+ const newCursor = this [ kGetDisplayPos ] ( this . line ) ;
1086+ // Go back to where the cursor was, with relative movement
1087+ moveCursor ( this . output , oldCursor . cols - newCursor . cols , oldCursor . rows - newCursor . rows ) ;
1088+ // Setting how many rows we have on top of the cursor
1089+ // Necessary for kRefreshLine
1090+ this . prevRows = oldCursor . rows ;
1091+ } else {
1092+ // Setting how many rows we have on top of the cursor
1093+ // Necessary for kRefreshLine
1094+ this . prevRows = StringPrototypeSplit ( this . line , '\n' ) . length - 1 ;
1095+ }
1096+ }
1097+
9571098 [ kPushToUndoStack ] ( text , cursor ) {
9581099 if ( ArrayPrototypePush ( this [ kUndoStack ] , { text, cursor } ) >
9591100 kMaxUndoRedoStackSize ) {
@@ -1525,6 +1666,7 @@ module.exports = {
15251666 kWordRight,
15261667 kWriteToOutput,
15271668 kMultilinePrompt,
1669+ kRestorePreviousState,
1670+ kAddNewLineOnTTY,
15281671 kLastCommandErrored,
1529- kNormalizeHistoryLineEndings,
15301672} ;
0 commit comments