77 ArrayPrototypeJoin,
88 ArrayPrototypeMap,
99 ArrayPrototypePop,
10+ ArrayPrototypePush,
1011 ArrayPrototypeReverse,
1112 ArrayPrototypeSplice,
13+ ArrayPrototypeShift,
1214 ArrayPrototypeUnshift,
1315 DateNow,
1416 FunctionPrototypeCall,
@@ -68,6 +70,7 @@ const { StringDecoder } = require('string_decoder');
6870let Readable ;
6971
7072const kHistorySize = 30 ;
73+ const kMaxUndoRedoStackSize = 2048 ;
7174const kMincrlfDelay = 100 ;
7275// \r\n, \n, or \r followed by something other than \n
7376const lineEnding = / \r ? \n | \r (? ! \n ) / ;
@@ -79,6 +82,7 @@ const kQuestionCancel = Symbol('kQuestionCancel');
7982const ESCAPE_CODE_TIMEOUT = 500 ;
8083
8184const kAddHistory = Symbol ( '_addHistory' ) ;
85+ const kBeforeEdit = Symbol ( '_beforeEdit' ) ;
8286const kDecoder = Symbol ( '_decoder' ) ;
8387const kDeleteLeft = Symbol ( '_deleteLeft' ) ;
8488const kDeleteLineLeft = Symbol ( '_deleteLineLeft' ) ;
@@ -98,14 +102,19 @@ const kOldPrompt = Symbol('_oldPrompt');
98102const kOnLine = Symbol ( '_onLine' ) ;
99103const kPreviousKey = Symbol ( '_previousKey' ) ;
100104const kPrompt = Symbol ( '_prompt' ) ;
105+ const kPushToUndoStack = Symbol ( '_pushToUndoStack' ) ;
101106const kQuestionCallback = Symbol ( '_questionCallback' ) ;
107+ const kRedo = Symbol ( '_redo' ) ;
108+ const kRedoStack = Symbol ( '_redoStack' ) ;
102109const kRefreshLine = Symbol ( '_refreshLine' ) ;
103110const kSawKeyPress = Symbol ( '_sawKeyPress' ) ;
104111const kSawReturnAt = Symbol ( '_sawReturnAt' ) ;
105112const kSetRawMode = Symbol ( '_setRawMode' ) ;
106113const kTabComplete = Symbol ( '_tabComplete' ) ;
107114const kTabCompleter = Symbol ( '_tabCompleter' ) ;
108115const kTtyWrite = Symbol ( '_ttyWrite' ) ;
116+ const kUndo = Symbol ( '_undo' ) ;
117+ const kUndoStack = Symbol ( '_undoStack' ) ;
109118const kWordLeft = Symbol ( '_wordLeft' ) ;
110119const kWordRight = Symbol ( '_wordRight' ) ;
111120const kWriteToOutput = Symbol ( '_writeToOutput' ) ;
@@ -198,6 +207,8 @@ function InterfaceConstructor(input, output, completer, terminal) {
198207 this [ kSubstringSearch ] = null ;
199208 this . output = output ;
200209 this . input = input ;
210+ this [ kUndoStack ] = [ ] ;
211+ this [ kRedoStack ] = [ ] ;
201212 this . history = history ;
202213 this . historySize = historySize ;
203214 this . removeHistoryDuplicates = ! ! removeHistoryDuplicates ;
@@ -390,6 +401,10 @@ class Interface extends InterfaceConstructor {
390401 }
391402 }
392403
404+ [ kBeforeEdit ] ( oldText , oldCursor ) {
405+ this [ kPushToUndoStack ] ( oldText , oldCursor ) ;
406+ }
407+
393408 [ kQuestionCancel ] ( ) {
394409 if ( this [ kQuestionCallback ] ) {
395410 this [ kQuestionCallback ] = null ;
@@ -579,6 +594,7 @@ class Interface extends InterfaceConstructor {
579594 }
580595
581596 [ kInsertString ] ( c ) {
597+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
582598 if ( this . cursor < this . line . length ) {
583599 const beg = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
584600 const end = StringPrototypeSlice (
@@ -648,6 +664,8 @@ class Interface extends InterfaceConstructor {
648664 return ;
649665 }
650666
667+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
668+
651669 // Apply/show completions.
652670 const completionsWidth = ArrayPrototypeMap ( completions , ( e ) =>
653671 getStringWidth ( e )
@@ -708,6 +726,7 @@ class Interface extends InterfaceConstructor {
708726
709727 [ kDeleteLeft ] ( ) {
710728 if ( this . cursor > 0 && this . line . length > 0 ) {
729+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
711730 // The number of UTF-16 units comprising the character to the left
712731 const charSize = charLengthLeft ( this . line , this . cursor ) ;
713732 this . line =
@@ -721,6 +740,7 @@ class Interface extends InterfaceConstructor {
721740
722741 [ kDeleteRight ] ( ) {
723742 if ( this . cursor < this . line . length ) {
743+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
724744 // The number of UTF-16 units comprising the character to the left
725745 const charSize = charLengthAt ( this . line , this . cursor ) ;
726746 this . line =
@@ -736,6 +756,7 @@ class Interface extends InterfaceConstructor {
736756
737757 [ kDeleteWordLeft ] ( ) {
738758 if ( this . cursor > 0 ) {
759+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
739760 // Reverse the string and match a word near beginning
740761 // to avoid quadratic time complexity
741762 let leading = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
@@ -759,6 +780,7 @@ class Interface extends InterfaceConstructor {
759780
760781 [ kDeleteWordRight ] ( ) {
761782 if ( this . cursor < this . line . length ) {
783+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
762784 const trailing = StringPrototypeSlice ( this . line , this . cursor ) ;
763785 const match = StringPrototypeMatch ( trailing , / ^ (?: \s + | \W + | \w + ) \s * / ) ;
764786 this . line =
@@ -769,12 +791,14 @@ class Interface extends InterfaceConstructor {
769791 }
770792
771793 [ kDeleteLineLeft ] ( ) {
794+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
772795 this . line = StringPrototypeSlice ( this . line , this . cursor ) ;
773796 this . cursor = 0 ;
774797 this [ kRefreshLine ] ( ) ;
775798 }
776799
777800 [ kDeleteLineRight ] ( ) {
801+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
778802 this . line = StringPrototypeSlice ( this . line , 0 , this . cursor ) ;
779803 this [ kRefreshLine ] ( ) ;
780804 }
@@ -789,10 +813,43 @@ class Interface extends InterfaceConstructor {
789813
790814 [ kLine ] ( ) {
791815 const line = this [ kAddHistory ] ( ) ;
816+ this [ kUndoStack ] = [ ] ;
817+ this [ kRedoStack ] = [ ] ;
792818 this . clearLine ( ) ;
793819 this [ kOnLine ] ( line ) ;
794820 }
795821
822+ [ kPushToUndoStack ] ( text , cursor ) {
823+ if ( ArrayPrototypePush ( this [ kUndoStack ] , { text, cursor } ) >
824+ kMaxUndoRedoStackSize ) {
825+ ArrayPrototypeShift ( this [ kUndoStack ] ) ;
826+ }
827+ }
828+
829+ [ kUndo ] ( ) {
830+ if ( this [ kUndoStack ] . length <= 0 ) return ;
831+
832+ const entry = this [ kUndoStack ] . pop ( ) ;
833+
834+ this . line = entry . text ;
835+ this . cursor = entry . cursor ;
836+
837+ ArrayPrototypePush ( this [ kRedoStack ] , entry ) ;
838+ this [ kRefreshLine ] ( ) ;
839+ }
840+
841+ [ kRedo ] ( ) {
842+ if ( this [ kRedoStack ] . length <= 0 ) return ;
843+
844+ const entry = this [ kRedoStack ] . pop ( ) ;
845+
846+ this . line = entry . text ;
847+ this . cursor = entry . cursor ;
848+
849+ ArrayPrototypePush ( this [ kUndoStack ] , entry ) ;
850+ this [ kRefreshLine ] ( ) ;
851+ }
852+
796853 // TODO(BridgeAR): Add underscores to the search part and a red background in
797854 // case no match is found. This should only be the visual part and not the
798855 // actual line content!
@@ -802,6 +859,7 @@ class Interface extends InterfaceConstructor {
802859 // one.
803860 [ kHistoryNext ] ( ) {
804861 if ( this . historyIndex >= 0 ) {
862+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
805863 const search = this [ kSubstringSearch ] || '' ;
806864 let index = this . historyIndex - 1 ;
807865 while (
@@ -824,6 +882,7 @@ class Interface extends InterfaceConstructor {
824882
825883 [ kHistoryPrev ] ( ) {
826884 if ( this . historyIndex < this . history . length && this . history . length ) {
885+ this [ kBeforeEdit ] ( this . line , this . cursor ) ;
827886 const search = this [ kSubstringSearch ] || '' ;
828887 let index = this . historyIndex + 1 ;
829888 while (
@@ -947,6 +1006,13 @@ class Interface extends InterfaceConstructor {
9471006 }
9481007 }
9491008
1009+ // Undo
1010+ if ( typeof key . sequence === 'string' &&
1011+ StringPrototypeCodePointAt ( key . sequence , 0 ) === 0x1f ) {
1012+ this [ kUndo ] ( ) ;
1013+ return ;
1014+ }
1015+
9501016 // Ignore escape key, fixes
9511017 // https://github.com/nodejs/node-v0.x-archive/issues/2876.
9521018 if ( key . name === 'escape' ) return ;
0 commit comments