Skip to content

Commit 0a31149

Browse files
rayw000BethGriggs
authored andcommitted
readline: add feature yank and yank pop
1. `Ctrl-Y` to yank previously deleted text 2. `Meta-Y` to do yank pop (cycle among deleted texts) 3. Use `getCursorPos().rows` to check if we have reached a new line, instead of `getCursorPos().cols === 0`. 4. document and unittests. PR-URL: #41301Fixes: #41252 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Qingyu Deng <[email protected]>
1 parent 81e039f commit 0a31149

File tree

3 files changed

+155
-1
lines changed

3 files changed

+155
-1
lines changed

‎doc/api/readline.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,16 @@ const{createInterface } = require('readline');
13131313
<td>Delete from the current position to the end of line</td>
13141314
<td></td>
13151315
</tr>
1316+
<tr>
1317+
<td><kbd>Ctrl</kbd>+<kbd>Y</kbd></td>
1318+
<td>Yank (Recall) the previously deleted text</td>
1319+
<td>Only works with text deleted by <kbd>Ctrl</kbd>+<kbd>U</kbd> or <kbd>Ctrl</kbd>+<kbd>K</kbd></td>
1320+
</tr>
1321+
<tr>
1322+
<td><kbd>Meta</kbd>+<kbd>Y</kbd></td>
1323+
<td>Cycle among previously deleted lines</td>
1324+
<td>Only available when the last keystroke is <kbd>Ctrl</kbd>+<kbd>Y</kbd></td>
1325+
</tr>
13161326
<tr>
13171327
<td><kbd>Ctrl</kbd>+<kbd>A</kbd></td>
13181328
<td>Go to start of line</td>

‎lib/internal/readline/interface.js‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ const kQuestionCancel = Symbol('kQuestionCancel');
8181
// GNU readline library - keyseq-timeout is 500ms (default)
8282
constESCAPE_CODE_TIMEOUT=500;
8383

84+
// Max length of the kill ring
85+
constkMaxLengthOfKillRing=32;
86+
8487
constkAddHistory=Symbol('_addHistory');
8588
constkBeforeEdit=Symbol('_beforeEdit');
8689
constkDecoder=Symbol('_decoder');
@@ -96,12 +99,15 @@ const kHistoryPrev = Symbol('_historyPrev');
9699
constkInsertString=Symbol('_insertString');
97100
constkLine=Symbol('_line');
98101
constkLine_buffer=Symbol('_line_buffer');
102+
constkKillRing=Symbol('_killRing');
103+
constkKillRingCursor=Symbol('_killRingCursor');
99104
constkMoveCursor=Symbol('_moveCursor');
100105
constkNormalWrite=Symbol('_normalWrite');
101106
constkOldPrompt=Symbol('_oldPrompt');
102107
constkOnLine=Symbol('_onLine');
103108
constkPreviousKey=Symbol('_previousKey');
104109
constkPrompt=Symbol('_prompt');
110+
constkPushToKillRing=Symbol('_pushToKillRing');
105111
constkPushToUndoStack=Symbol('_pushToUndoStack');
106112
constkQuestionCallback=Symbol('_questionCallback');
107113
constkRedo=Symbol('_redo');
@@ -118,6 +124,9 @@ const kUndoStack = Symbol('_undoStack');
118124
constkWordLeft=Symbol('_wordLeft');
119125
constkWordRight=Symbol('_wordRight');
120126
constkWriteToOutput=Symbol('_writeToOutput');
127+
constkYank=Symbol('_yank');
128+
constkYanking=Symbol('_yanking');
129+
constkYankPop=Symbol('_yankPop');
121130

122131
functionInterfaceConstructor(input,output,completer,terminal){
123132
this[kSawReturnAt]=0;
@@ -211,6 +220,15 @@ function InterfaceConstructor(input, output, completer, terminal){
211220
this[kRedoStack]=[];
212221
this.history=history;
213222
this.historySize=historySize;
223+
224+
// The kill ring is a global list of blocks of text that were previously
225+
// killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest
226+
// element will be removed to make room for the latest deletion. With kill
227+
// ring, users are able to recall (yank) or cycle (yank pop) among previously
228+
// killed texts, quite similar to the behavior of Emacs.
229+
this[kKillRing]=[];
230+
this[kKillRingCursor]=0;
231+
214232
this.removeHistoryDuplicates=!!removeHistoryDuplicates;
215233
this.crlfDelay=crlfDelay ?
216234
MathMax(kMincrlfDelay,crlfDelay) :
@@ -606,10 +624,12 @@ class Interface extends InterfaceConstructor{
606624
this.cursor+=c.length;
607625
this[kRefreshLine]();
608626
}else{
627+
constoldPos=this.getCursorPos();
609628
this.line+=c;
610629
this.cursor+=c.length;
630+
constnewPos=this.getCursorPos();
611631

612-
if(this.getCursorPos().cols===0){
632+
if(oldPos.rows<newPos.rows){
613633
this[kRefreshLine]();
614634
}else{
615635
this[kWriteToOutput](c);
@@ -792,17 +812,57 @@ class Interface extends InterfaceConstructor{
792812

793813
[kDeleteLineLeft](){
794814
this[kBeforeEdit](this.line,this.cursor);
815+
constdel=StringPrototypeSlice(this.line,0,this.cursor);
795816
this.line=StringPrototypeSlice(this.line,this.cursor);
796817
this.cursor=0;
818+
this[kPushToKillRing](del);
797819
this[kRefreshLine]();
798820
}
799821

800822
[kDeleteLineRight](){
801823
this[kBeforeEdit](this.line,this.cursor);
824+
constdel=StringPrototypeSlice(this.line,this.cursor);
802825
this.line=StringPrototypeSlice(this.line,0,this.cursor);
826+
this[kPushToKillRing](del);
803827
this[kRefreshLine]();
804828
}
805829

830+
[kPushToKillRing](del){
831+
if(!del||del===this[kKillRing][0])return;
832+
ArrayPrototypeUnshift(this[kKillRing],del);
833+
this[kKillRingCursor]=0;
834+
while(this[kKillRing].length>kMaxLengthOfKillRing)
835+
ArrayPrototypePop(this[kKillRing]);
836+
}
837+
838+
[kYank](){
839+
if(this[kKillRing].length>0){
840+
this[kYanking]=true;
841+
this[kInsertString](this[kKillRing][this[kKillRingCursor]]);
842+
}
843+
}
844+
845+
[kYankPop](){
846+
if(!this[kYanking]){
847+
return;
848+
}
849+
if(this[kKillRing].length>1){
850+
constlastYank=this[kKillRing][this[kKillRingCursor]];
851+
this[kKillRingCursor]++;
852+
if(this[kKillRingCursor]>=this[kKillRing].length){
853+
this[kKillRingCursor]=0;
854+
}
855+
constcurrentYank=this[kKillRing][this[kKillRingCursor]];
856+
consthead=
857+
StringPrototypeSlice(this.line,0,this.cursor-lastYank.length);
858+
consttail=
859+
StringPrototypeSlice(this.line,this.cursor);
860+
this.line=head+currentYank+tail;
861+
this.cursor=head.length+currentYank.length;
862+
this[kRefreshLine]();
863+
}
864+
}
865+
806866
clearLine(){
807867
this[kMoveCursor](+Infinity);
808868
this[kWriteToOutput]('\r\n');
@@ -984,6 +1044,11 @@ class Interface extends InterfaceConstructor{
9841044
key=key||{};
9851045
this[kPreviousKey]=key;
9861046

1047+
if(!key.meta||key.name!=='y'){
1048+
// Reset yanking state unless we are doing yank pop.
1049+
this[kYanking]=false;
1050+
}
1051+
9871052
// Activate or deactivate substring search.
9881053
if(
9891054
(key.name==='up'||key.name==='down')&&
@@ -1094,6 +1159,10 @@ class Interface extends InterfaceConstructor{
10941159
this[kHistoryPrev]();
10951160
break;
10961161

1162+
case'y': // Yank killed string
1163+
this[kYank]();
1164+
break;
1165+
10971166
case'z':
10981167
if(process.platform==='win32')break;
10991168
if(this.listenerCount('SIGTSTP')>0){
@@ -1158,6 +1227,10 @@ class Interface extends InterfaceConstructor{
11581227
case'backspace': // Delete backwards to a word boundary
11591228
this[kDeleteWordLeft]();
11601229
break;
1230+
1231+
case'y': // Doing yank pop
1232+
this[kYankPop]();
1233+
break;
11611234
}
11621235
}else{
11631236
/* No modifier keys used */

‎test/parallel/test-readline-interface.js‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,77 @@ function assertCursorRowsAndCols(rli, rows, cols){
674674
rli.close();
675675
}
676676

677+
// yank
678+
{
679+
const[rli,fi]=getInterface({terminal: true,prompt: ''});
680+
fi.emit('data','the quick brown fox');
681+
assertCursorRowsAndCols(rli,0,19);
682+
683+
// Go to the start of the line
684+
fi.emit('keypress','.',{ctrl: true,name: 'a'});
685+
// Move forward one char
686+
fi.emit('keypress','.',{ctrl: true,name: 'f'});
687+
// Delete the right part
688+
fi.emit('keypress','.',{ctrl: true,shift: true,name: 'delete'});
689+
assertCursorRowsAndCols(rli,0,1);
690+
691+
// Yank
692+
fi.emit('keypress','.',{ctrl: true,name: 'y'});
693+
assertCursorRowsAndCols(rli,0,19);
694+
695+
rli.on('line',common.mustCall((line)=>{
696+
assert.strictEqual(line,'the quick brown fox');
697+
}));
698+
699+
fi.emit('data','\n');
700+
rli.close();
701+
}
702+
703+
// yank pop
704+
{
705+
const[rli,fi]=getInterface({terminal: true,prompt: ''});
706+
fi.emit('data','the quick brown fox');
707+
assertCursorRowsAndCols(rli,0,19);
708+
709+
// Go to the start of the line
710+
fi.emit('keypress','.',{ctrl: true,name: 'a'});
711+
// Move forward one char
712+
fi.emit('keypress','.',{ctrl: true,name: 'f'});
713+
// Delete the right part
714+
fi.emit('keypress','.',{ctrl: true,shift: true,name: 'delete'});
715+
assertCursorRowsAndCols(rli,0,1);
716+
// Yank
717+
fi.emit('keypress','.',{ctrl: true,name: 'y'});
718+
assertCursorRowsAndCols(rli,0,19);
719+
720+
// Go to the start of the line
721+
fi.emit('keypress','.',{ctrl: true,name: 'a'});
722+
// Move forward four chars
723+
fi.emit('keypress','.',{ctrl: true,name: 'f'});
724+
fi.emit('keypress','.',{ctrl: true,name: 'f'});
725+
fi.emit('keypress','.',{ctrl: true,name: 'f'});
726+
fi.emit('keypress','.',{ctrl: true,name: 'f'});
727+
// Delete the right part
728+
fi.emit('keypress','.',{ctrl: true,shift: true,name: 'delete'});
729+
assertCursorRowsAndCols(rli,0,4);
730+
// Go to the start of the line
731+
fi.emit('keypress','.',{ctrl: true,name: 'a'});
732+
assertCursorRowsAndCols(rli,0,0);
733+
734+
// Yank: 'quick brown fox|the '
735+
fi.emit('keypress','.',{ctrl: true,name: 'y'});
736+
// Yank pop: 'he quick brown fox|the'
737+
fi.emit('keypress','.',{meta: true,name: 'y'});
738+
assertCursorRowsAndCols(rli,0,18);
739+
740+
rli.on('line',common.mustCall((line)=>{
741+
assert.strictEqual(line,'he quick brown foxthe ');
742+
}));
743+
744+
fi.emit('data','\n');
745+
rli.close();
746+
}
747+
677748
// Close readline interface
678749
{
679750
const[rli,fi]=getInterface({terminal: true,prompt: ''});

0 commit comments

Comments
(0)