2727import { op_get_env_no_permission_check } from "ext:core/ops" ;
2828
2929import {
30+ ERR_INVALID_ARG_TYPE ,
3031 ERR_INVALID_ARG_VALUE ,
32+ ERR_OUT_OF_RANGE ,
3133 ERR_USE_AFTER_CLOSE ,
3234} from "ext:deno_node/internal/errors.ts" ;
3335import {
@@ -118,6 +120,15 @@ const lineEnding = new SafeRegExp(/\r?\n|\r(?!\n)|\u2028|\u2029/g);
118120const kLineObjectStream = Symbol ( "line object stream" ) ;
119121export const kQuestionCancel = Symbol ( "kQuestionCancel" ) ;
120122export const kQuestion = Symbol ( "kQuestion" ) ;
123+ const kKillRing = Symbol ( "kKillRing" ) ;
124+ const kKillRingCursor = Symbol ( "kKillRingCursor" ) ;
125+ const kYank = Symbol ( "kYank" ) ;
126+ const kYankPop = Symbol ( "kYankPop" ) ;
127+ const kUndoStack = Symbol ( "kUndoStack" ) ;
128+ const kRedoStack = Symbol ( "kRedoStack" ) ;
129+ const kUndo = Symbol ( "kUndo" ) ;
130+ const kRedo = Symbol ( "kRedo" ) ;
131+ const kPushToUndoStack = Symbol ( "kPushToUndoStack" ) ;
121132
122133// GNU readline library - keyseq-timeout is 500ms (default)
123134const ESCAPE_CODE_TIMEOUT = 500 ;
@@ -223,12 +234,12 @@ export function InterfaceConstructor(input, output, completer, terminal) {
223234 historySize = kHistorySize ;
224235 }
225236
226- if (
227- typeof historySize !== "number" ||
228- Number . isNaN ( historySize ) ||
229- historySize < 0
230- ) {
231- throw new ERR_INVALID_ARG_VALUE . RangeError ( "historySize" , historySize ) ;
237+ if ( typeof historySize !== "number" ) {
238+ throw new ERR_INVALID_ARG_TYPE ( " historySize" , "number" , historySize ) ;
239+ }
240+
241+ if ( Number . isNaN ( historySize ) || historySize < 0 ) {
242+ throw new ERR_OUT_OF_RANGE ( "historySize" , ">= 0 ", historySize ) ;
232243 }
233244
234245 // Backwards compat; check the isTTY prop of the output stream
@@ -250,6 +261,10 @@ export function InterfaceConstructor(input, output, completer, terminal) {
250261 ? Math . max ( kMincrlfDelay , crlfDelay )
251262 : kMincrlfDelay ;
252263 this . completer = completer ;
264+ this [ kKillRing ] = [ ] ;
265+ this [ kKillRingCursor ] = 0 ;
266+ this [ kUndoStack ] = [ ] ;
267+ this [ kRedoStack ] = [ ] ;
253268
254269 this . setPrompt ( prompt ) ;
255270
@@ -581,6 +596,9 @@ export class Interface extends InterfaceConstructor {
581596 * @returns {void }
582597 */
583598 write ( d , key ) {
599+ if ( this . closed ) {
600+ throw new ERR_USE_AFTER_CLOSE ( "readline" ) ;
601+ }
584602 if ( this . paused ) this . resume ( ) ;
585603 if ( this . terminal ) {
586604 this [ kTtyWrite ] ( d , key ) ;
@@ -709,7 +727,7 @@ export class Interface extends InterfaceConstructor {
709727 const completionsWidth = completions . map (
710728 ( e ) => getStringWidth ( e ) ,
711729 ) ;
712- const width = Math . max . apply ( completionsWidth ) + 2 ; // 2 space padding
730+ const width = Math . max ( ... completionsWidth ) + 2 ; // 2 space padding
713731 let maxColumns = Math . floor ( this . columns / width ) || 1 ;
714732 if ( maxColumns === Infinity ) {
715733 maxColumns = 1 ;
@@ -762,6 +780,7 @@ export class Interface extends InterfaceConstructor {
762780
763781 [ kDeleteLeft ] ( ) {
764782 if ( this . cursor > 0 && this . line . length > 0 ) {
783+ this [ kPushToUndoStack ] ( ) ;
765784 // The number of UTF-16 units comprising the character to the left
766785 const charSize = charLengthLeft ( this . line , this . cursor ) ;
767786 this . line = this . line . slice ( 0 , this . cursor - charSize ) +
@@ -774,6 +793,7 @@ export class Interface extends InterfaceConstructor {
774793
775794 [ kDeleteRight ] ( ) {
776795 if ( this . cursor < this . line . length ) {
796+ this [ kPushToUndoStack ] ( ) ;
777797 // The number of UTF-16 units comprising the character to the left
778798 const charSize = charLengthAt ( this . line , this . cursor ) ;
779799 this . line = this . line . slice ( 0 , this . cursor ) +
@@ -787,6 +807,7 @@ export class Interface extends InterfaceConstructor {
787807
788808 [ kDeleteWordLeft ] ( ) {
789809 if ( this . cursor > 0 ) {
810+ this [ kPushToUndoStack ] ( ) ;
790811 // Reverse the string and match a word near beginning
791812 // to avoid quadratic time complexity
792813 let leading = this . line . slice ( 0 , this . cursor ) ;
@@ -796,34 +817,100 @@ export class Interface extends InterfaceConstructor {
796817 0 ,
797818 leading . length - match [ 0 ] . length ,
798819 ) ;
820+ const killed = this . line . slice ( leading . length , this . cursor ) ;
799821 this . line = leading +
800822 this . line . slice ( this . cursor , this . line . length ) ;
801823 this . cursor = leading . length ;
824+ this [ kKillRing ] . push ( killed ) ;
825+ this [ kKillRingCursor ] = this [ kKillRing ] . length - 1 ;
802826 this [ kRefreshLine ] ( ) ;
803827 }
804828 }
805829
806830 [ kDeleteWordRight ] ( ) {
807831 if ( this . cursor < this . line . length ) {
832+ this [ kPushToUndoStack ] ( ) ;
808833 const trailing = this . line . slice ( this . cursor ) ;
809834 const match = trailing . match ( / ^ (?: \s + | \W + | \w + ) \s * / ) ;
835+ const killed = trailing . slice ( 0 , match [ 0 ] . length ) ;
810836 this . line = this . line . slice ( 0 , this . cursor ) +
811837 trailing . slice ( match [ 0 ] . length ) ;
838+ this [ kKillRing ] . push ( killed ) ;
839+ this [ kKillRingCursor ] = this [ kKillRing ] . length - 1 ;
812840 this [ kRefreshLine ] ( ) ;
813841 }
814842 }
815843
816844 [ kDeleteLineLeft ] ( ) {
845+ this [ kPushToUndoStack ] ( ) ;
846+ const killed = this . line . slice ( 0 , this . cursor ) ;
817847 this . line = this . line . slice ( this . cursor ) ;
818848 this . cursor = 0 ;
849+ this [ kKillRing ] . push ( killed ) ;
850+ this [ kKillRingCursor ] = this [ kKillRing ] . length - 1 ;
819851 this [ kRefreshLine ] ( ) ;
820852 }
821853
822854 [ kDeleteLineRight ] ( ) {
855+ this [ kPushToUndoStack ] ( ) ;
856+ const killed = this . line . slice ( this . cursor ) ;
823857 this . line = this . line . slice ( 0 , this . cursor ) ;
858+ this [ kKillRing ] . push ( killed ) ;
859+ this [ kKillRingCursor ] = this [ kKillRing ] . length - 1 ;
824860 this [ kRefreshLine ] ( ) ;
825861 }
826862
863+ [ kYank ] ( ) {
864+ if ( this [ kKillRing ] . length > 0 ) {
865+ this [ kKillRingCursor ] = this [ kKillRing ] . length - 1 ;
866+ const killed = this [ kKillRing ] [ this [ kKillRingCursor ] ] ;
867+ this [ kInsertString ] ( killed ) ;
868+ }
869+ }
870+
871+ [ kYankPop ] ( ) {
872+ if ( this [ kKillRing ] . length > 1 ) {
873+ // Remove previously yanked text
874+ const prev = this [ kKillRing ] [ this [ kKillRingCursor ] ] ;
875+ this . line = this . line . slice ( 0 , this . cursor - prev . length ) +
876+ this . line . slice ( this . cursor ) ;
877+ this . cursor -= prev . length ;
878+
879+ // Cycle to previous entry in kill ring
880+ this [ kKillRingCursor ] -- ;
881+ if ( this [ kKillRingCursor ] < 0 ) {
882+ this [ kKillRingCursor ] = this [ kKillRing ] . length - 1 ;
883+ }
884+ const killed = this [ kKillRing ] [ this [ kKillRingCursor ] ] ;
885+ this [ kInsertString ] ( killed ) ;
886+ }
887+ }
888+
889+ [ kPushToUndoStack ] ( ) {
890+ this [ kUndoStack ] . push ( { line : this . line , cursor : this . cursor } ) ;
891+ this [ kRedoStack ] = [ ] ;
892+ }
893+
894+ [ kUndo ] ( ) {
895+ if ( this [ kUndoStack ] . length > 0 ) {
896+ this [ kRedoStack ] . push ( { line : this . line , cursor : this . cursor } ) ;
897+ const state = this [ kUndoStack ] . pop ( ) ;
898+ this . line = state . line ;
899+ this . cursor = state . cursor ;
900+ this [ kRefreshLine ] ( ) ;
901+ }
902+ }
903+
904+ [ kRedo ] ( ) {
905+ if ( this [ kRedoStack ] . length > 0 ) {
906+ this [ kUndoStack ] . push ( { line : this . line , cursor : this . cursor } ) ;
907+ const state = this [ kRedoStack ] . pop ( ) ;
908+ this . line = state . line ;
909+ this . cursor = state . cursor ;
910+ this [ kRefreshLine ] ( ) ;
911+ }
912+ }
913+
827914 clearLine ( ) {
828915 this [ kMoveCursor ] ( + Infinity ) ;
829916 this [ kWriteToOutput ] ( "\r\n" ) ;
@@ -991,6 +1078,16 @@ export class Interface extends InterfaceConstructor {
9911078 }
9921079 }
9931080
1081+ // Undo (Ctrl+_) and Redo (Ctrl+^)
1082+ if ( key . sequence === "\x1F" ) {
1083+ this [ kUndo ] ( ) ;
1084+ return ;
1085+ }
1086+ if ( key . sequence === "\x1E" ) {
1087+ this [ kRedo ] ( ) ;
1088+ return ;
1089+ }
1090+
9941091 // Ignore escape key, fixes
9951092 // https://github.com/nodejs/node-v0.x-archive/issues/2876.
9961093 if ( key . name === "escape" ) return ;
@@ -1072,6 +1169,10 @@ export class Interface extends InterfaceConstructor {
10721169 this [ kHistoryPrev ] ( ) ;
10731170 break ;
10741171
1172+ case "y" : // Yank killed text
1173+ this [ kYank ] ( ) ;
1174+ break ;
1175+
10751176 case "z" :
10761177 if ( process . platform === "win32" ) break ;
10771178 if ( this . listenerCount ( "SIGTSTP" ) > 0 ) {
@@ -1136,6 +1237,10 @@ export class Interface extends InterfaceConstructor {
11361237 case "backspace" : // Delete backwards to a word boundary
11371238 this [ kDeleteWordLeft ] ( ) ;
11381239 break ;
1240+
1241+ case "y" : // Yank pop
1242+ this [ kYankPop ] ( ) ;
1243+ break ;
11391244 }
11401245 } else {
11411246 /* No modifier keys used */
0 commit comments