@@ -100,7 +100,6 @@ const kMaxLengthOfKillRing = 32;
100100
101101// 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,8 @@ const kPrompt = Symbol('_prompt');
131130const kPushToKillRing = Symbol ( '_pushToKillRing' ) ;
132131const kPushToUndoStack = Symbol ( '_pushToUndoStack' ) ;
133132const kQuestionCallback = Symbol ( '_questionCallback' ) ;
133+ const kReverseString = Symbol ( '_reverseString' ) ;
134+ const kLastCommandErrored = Symbol ( '_lastCommandErrored' ) ;
134135const kQuestionReject = Symbol ( '_questionReject' ) ;
135136const kRedo = Symbol ( '_redo' ) ;
136137const kRedoStack = Symbol ( '_redoStack' ) ;
@@ -151,6 +152,12 @@ const kYank = Symbol('_yank');
151152const kYanking = Symbol ( '_yanking' ) ;
152153const kYankPop = Symbol ( '_yankPop' ) ;
153154const kNormalizeHistoryLineEndings = Symbol ( '_normalizeHistoryLineEndings' ) ;
155+ const kSavePreviousState = Symbol ( '_savePreviousState' ) ;
156+ const kRestorePreviousState = Symbol ( '_restorePreviousState' ) ;
157+ const kPreviousLine = Symbol ( '_previousLine' ) ;
158+ const kPreviousCursor = Symbol ( '_previousCursor' ) ;
159+ const kPreviousPrevRows = Symbol ( '_previousPrevRows' ) ;
160+ const kAddNewLineOnTTY = Symbol ( '_addNewLineOnTTY' ) ;
154161
155162function InterfaceConstructor ( input , output , completer , terminal ) {
156163 this [ kSawReturnAt ] = 0 ;
@@ -430,7 +437,7 @@ class Interface extends InterfaceConstructor {
430437 }
431438 }
432439
433- [ kSetLine ] ( line ) {
440+ [ kSetLine ] ( line = '' ) {
434441 this . line = line ;
435442 this [ kIsMultiline ] = StringPrototypeIncludes ( line , '\n' ) ;
436443 }
@@ -477,15 +484,26 @@ class Interface extends InterfaceConstructor {
477484 // Reversing the multilines is necessary when adding / editing and displaying them
478485 if ( reverse ) {
479486 // First reverse the lines for proper order, then convert separators
480- return ArrayPrototypeJoin (
481- ArrayPrototypeReverse ( StringPrototypeSplit ( line , from ) ) ,
482- to ,
483- ) ;
487+ return this [ kReverseString ] ( line , from , to ) ;
484488 }
485489 // For normal cases (saving to history or non-multiline entries)
486490 return StringPrototypeReplaceAll ( line , from , to ) ;
487491 }
488492
493+ [ kReverseString ] ( line , from , to ) {
494+ const parts = StringPrototypeSplit ( line , from ) ;
495+
496+ // This implementation should be faster than
497+ // ArrayPrototypeJoin(ArrayPrototypeReverse(StringPrototypeSplit(line, from)), to);
498+ let result = '' ;
499+ for ( let i = parts . length - 1 ; i > 0 ; i -- ) {
500+ result += parts [ i ] + to ;
501+ }
502+ result += parts [ 0 ] ;
503+
504+ return result ;
505+ }
506+
489507 [ kAddHistory ] ( ) {
490508 if ( this . line . length === 0 ) return '' ;
491509
@@ -494,22 +512,30 @@ class Interface extends InterfaceConstructor {
494512
495513 // If the trimmed line is empty then return the line
496514 if ( StringPrototypeTrim ( this . line ) . length === 0 ) return this . line ;
515+
516+ // This is necessary because each like would be saved in the history while creating
517+ // A new multiline, and we don't want that.
518+ if ( this [ kIsMultiline ] && this . historyIndex === - 1 ) {
519+ ArrayPrototypeShift ( this . history ) ;
520+ }
521+ // If the last command errored and we are trying to edit the history to fix it
522+ // Remove the broken one from the history
523+ if ( this [ kLastCommandErrored ] && this . history . length > 0 ) {
524+ ArrayPrototypeShift ( this . history ) ;
525+ }
526+
497527 const normalizedLine = this [ kNormalizeHistoryLineEndings ] ( this . line , '\n' , '\r' , false ) ;
498528
499529 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- }
506530 if ( this . removeHistoryDuplicates ) {
507531 // Remove older history line if identical to new one
508532 const dupIndex = ArrayPrototypeIndexOf ( this . history , this . line ) ;
509533 if ( dupIndex !== - 1 ) ArrayPrototypeSplice ( this . history , dupIndex , 1 ) ;
510534 }
511535
512- ArrayPrototypeUnshift ( this . history , this . line ) ;
536+ ArrayPrototypeUnshift ( this . history , this [ kIsMultiline ] ?
537+ this [ kReverseString ] ( normalizedLine , '\r' , '\r' ) :
538+ normalizedLine ) ;
513539
514540 // Only store so many
515541 if ( this . history . length > this . historySize )
@@ -521,7 +547,7 @@ class Interface extends InterfaceConstructor {
521547 // The listener could change the history object, possibly
522548 // to remove the last added entry if it is sensitive and should
523549 // not be persisted in the history, like a password
524- const line = this . history [ 0 ] ;
550+ const line = this [ kIsMultiline ] ? this [ kReverseString ] ( this . history [ 0 ] , '\r' , '\r' ) : this . history [ 0 ] ;
525551
526552 // Emit history event to notify listeners of update
527553 this . emit ( 'history' , this . history ) ;
@@ -938,6 +964,18 @@ class Interface extends InterfaceConstructor {
938964 }
939965 }
940966
967+ [ kSavePreviousState ] ( ) {
968+ this [ kPreviousLine ] = this . line ;
969+ this [ kPreviousCursor ] = this . cursor ;
970+ this [ kPreviousPrevRows ] = this . prevRows ;
971+ }
972+
973+ [ kRestorePreviousState ] ( ) {
974+ this [ kSetLine ] ( this [ kPreviousLine ] ) ;
975+ this . cursor = this [ kPreviousCursor ] ;
976+ this . prevRows = this [ kPreviousPrevRows ] ;
977+ }
978+
941979 clearLine ( ) {
942980 this [ kMoveCursor ] ( + Infinity ) ;
943981 this [ kWriteToOutput ] ( '\r\n' ) ;
@@ -947,13 +985,110 @@ class Interface extends InterfaceConstructor {
947985 }
948986
949987 [ kLine ] ( ) {
988+ this [ kSavePreviousState ] ( ) ;
950989 const line = this [ kAddHistory ] ( ) ;
951990 this [ kUndoStack ] = [ ] ;
952991 this [ kRedoStack ] = [ ] ;
953992 this . clearLine ( ) ;
954993 this [ kOnLine ] ( line ) ;
955994 }
956995
996+ // When this function is called, the actual cursor is at the very end of the whole string,
997+ // No matter where the new line was entered.
998+ // This function should only be used when the output is a TTY
999+ [ kAddNewLineOnTTY ] ( ) {
1000+ if ( ! this . terminal ) {
1001+ return ;
1002+ }
1003+
1004+ this [ kRestorePreviousState ] ( ) ;
1005+ const originalLine = this . line ;
1006+
1007+ // Before the cursor
1008+ const beg = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
1009+ // After the cursor
1010+ let end = StringPrototypeSlice ( this . line , this . cursor , this . line . length ) ;
1011+
1012+ // Add the new line where the cursor is at
1013+ this [ kSetLine ] ( `${ beg } \n${ end } ` ) ;
1014+
1015+ // To account for the new line
1016+ this . cursor += 1 ;
1017+
1018+ const hasContentAfterCursor = end . length > 0 ;
1019+ const cursorIsNotOnFirstLine = this . prevRows > 0 ;
1020+ const lineParts = hasContentAfterCursor ? StringPrototypeSplit ( beg , '\n' ) : null ;
1021+
1022+ // Handle cursor positioning based on different scenarios
1023+ if ( hasContentAfterCursor ) {
1024+ // If the cursor is not on the first line
1025+ if ( cursorIsNotOnFirstLine ) {
1026+ const splitEnd = StringPrototypeSplit ( end , '\n' ) ;
1027+ // If the cursor when I pressed enter was at least on the second line
1028+ // I need to completely erase the line where the cursor was pressed because it is possible
1029+ // That it was pressed in the middle of the line, hence I need to write the whole line.
1030+ // To achieve that, I need to reach the line above the current line coming from the end
1031+ const dy = splitEnd . length + 1 ;
1032+ // Calculate how many Xs we need to move on the right to get to the end of the line
1033+ const dxEndOfLineAbove = lineParts [ lineParts . length - 2 ] . length + kMultilinePrompt . description . length ;
1034+ moveCursor ( this . output , dxEndOfLineAbove , - dy ) ;
1035+ // This is the line that was split in the middle
1036+ // Just add it to the rest of the line that will be printed later
1037+ end = `${ lineParts [ lineParts . length - 1 ] } \n${ end } ` ;
1038+ } else {
1039+ // Otherwise, go to the very beginning of the first line and erase everything
1040+ const dy = StringPrototypeSplit ( originalLine , '\n' ) . length ;
1041+ moveCursor ( this . output , 0 , - dy ) ;
1042+ }
1043+ } else {
1044+ // Otherwise, go to the very beginning of the currentLine
1045+ // We need to rewrite everything
1046+ cursorTo ( this . output , 0 ) ;
1047+ }
1048+
1049+ // Erase from the cursor to the end of the line
1050+ clearScreenDown ( this . output ) ;
1051+
1052+ // Handle new line output for multiline case
1053+ if ( hasContentAfterCursor && cursorIsNotOnFirstLine ) {
1054+ this [ kWriteToOutput ] ( '\n' ) ;
1055+ }
1056+
1057+ // Determine if we need to rewrite the first line
1058+ const needsRewriteFirstLine = lineParts && lineParts . length < 2 ;
1059+ if ( needsRewriteFirstLine ) {
1060+ this [ kWriteToOutput ] ( `${ this [ kPrompt ] } ${ beg } \n${ kMultilinePrompt . description } ` ) ;
1061+ } else {
1062+ this [ kWriteToOutput ] ( kMultilinePrompt . description ) ;
1063+ }
1064+
1065+ // Write the rest and restore the cursor to where the user left it
1066+ if ( hasContentAfterCursor ) {
1067+ // Save the cursor pos, we need to come back here
1068+ const oldCursor = this . getCursorPos ( ) ;
1069+
1070+ // Write everything after the cursor which has been deleted by clearScreenDown
1071+ for ( const letter of end ) {
1072+ if ( letter === '\n' ) {
1073+ this [ kWriteToOutput ] ( `\n${ kMultilinePrompt . description } ` ) ;
1074+ } else {
1075+ this [ kWriteToOutput ] ( letter ) ;
1076+ }
1077+ }
1078+
1079+ const newCursor = this [ kGetDisplayPos ] ( this . line ) ;
1080+ // Go back to where the cursor was, with relative movement
1081+ moveCursor ( this . output , oldCursor . cols - newCursor . cols , oldCursor . rows - newCursor . rows ) ;
1082+ // Setting how many rows we have on top of the cursor
1083+ // Necessary for kRefreshLine
1084+ this . prevRows = oldCursor . rows ;
1085+ } else {
1086+ // Setting how many rows we have on top of the cursor
1087+ // Necessary for kRefreshLine
1088+ this . prevRows = StringPrototypeSplit ( this . line , '\n' ) . length - 1 ;
1089+ }
1090+ }
1091+
9571092 [ kPushToUndoStack ] ( text , cursor ) {
9581093 if ( ArrayPrototypePush ( this [ kUndoStack ] , { text, cursor } ) >
9591094 kMaxUndoRedoStackSize ) {
@@ -1370,6 +1505,14 @@ class Interface extends InterfaceConstructor {
13701505 case 'y' : // Doing yank pop
13711506 this [ kYankPop ] ( ) ;
13721507 break ;
1508+
1509+ case 'return' : // ((meta|option) + enter) Inserting a new line, this might work on few terminals only
1510+ // The next 2 lines are to standardize the behavior simulating what [kLine]() does.
1511+ // The internal calculations in [kAddNewLineOnTTY] are expecting certain state to be set.
1512+ this [ kSavePreviousState ] ( ) ;
1513+ this . clearLine ( ) ;
1514+ this [ kAddNewLineOnTTY ] ( ) ;
1515+ break ;
13731516 }
13741517 } else {
13751518 /* No modifier keys used */
@@ -1525,6 +1668,7 @@ module.exports = {
15251668 kWordRight,
15261669 kWriteToOutput,
15271670 kMultilinePrompt,
1671+ kRestorePreviousState,
1672+ kAddNewLineOnTTY,
15281673 kLastCommandErrored,
1529- kNormalizeHistoryLineEndings,
15301674} ;
0 commit comments