Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0a31149

Browse filesBrowse 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: #41301 Fixes: #41252 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Qingyu Deng <i@ayase-lab.com>
1 parent 81e039f commit 0a31149
Copy full SHA for 0a31149

File tree

Expand file treeCollapse file tree

3 files changed

+155
-1
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

3 files changed

+155
-1
lines changed
Open diff view settings
Collapse file

‎doc/api/readline.md‎

Copy file name to clipboardExpand all lines: doc/api/readline.md
+10Lines changed: 10 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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>
Collapse file

‎lib/internal/readline/interface.js‎

Copy file name to clipboardExpand all lines: lib/internal/readline/interface.js
+74-1Lines 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
const ESCAPE_CODE_TIMEOUT = 500;
8383

84+
// Max length of the kill ring
85+
const kMaxLengthOfKillRing = 32;
86+
8487
const kAddHistory = Symbol('_addHistory');
8588
const kBeforeEdit = Symbol('_beforeEdit');
8689
const kDecoder = Symbol('_decoder');
@@ -96,12 +99,15 @@ const kHistoryPrev = Symbol('_historyPrev');
9699
const kInsertString = Symbol('_insertString');
97100
const kLine = Symbol('_line');
98101
const kLine_buffer = Symbol('_line_buffer');
102+
const kKillRing = Symbol('_killRing');
103+
const kKillRingCursor = Symbol('_killRingCursor');
99104
const kMoveCursor = Symbol('_moveCursor');
100105
const kNormalWrite = Symbol('_normalWrite');
101106
const kOldPrompt = Symbol('_oldPrompt');
102107
const kOnLine = Symbol('_onLine');
103108
const kPreviousKey = Symbol('_previousKey');
104109
const kPrompt = Symbol('_prompt');
110+
const kPushToKillRing = Symbol('_pushToKillRing');
105111
const kPushToUndoStack = Symbol('_pushToUndoStack');
106112
const kQuestionCallback = Symbol('_questionCallback');
107113
const kRedo = Symbol('_redo');
@@ -118,6 +124,9 @@ const kUndoStack = Symbol('_undoStack');
118124
const kWordLeft = Symbol('_wordLeft');
119125
const kWordRight = Symbol('_wordRight');
120126
const kWriteToOutput = Symbol('_writeToOutput');
127+
const kYank = Symbol('_yank');
128+
const kYanking = Symbol('_yanking');
129+
const kYankPop = Symbol('_yankPop');
121130

122131
function InterfaceConstructor(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+
const oldPos = this.getCursorPos();
609628
this.line += c;
610629
this.cursor += c.length;
630+
const newPos = 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+
const del = 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+
const del = 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+
const lastYank = this[kKillRing][this[kKillRingCursor]];
851+
this[kKillRingCursor]++;
852+
if (this[kKillRingCursor] >= this[kKillRing].length) {
853+
this[kKillRingCursor] = 0;
854+
}
855+
const currentYank = this[kKillRing][this[kKillRingCursor]];
856+
const head =
857+
StringPrototypeSlice(this.line, 0, this.cursor - lastYank.length);
858+
const tail =
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 */
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-readline-interface.js
+71Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.