readline,repl: improve history up/previous

Reaching the history end caused the last entry to be persistent.
That way there's no actualy feedback to the user that the history
end is reached. Instead, visualize the original input line and keep
the history index at the history end in case the user wants to go
back again.

PR-URL: https://github.com/nodejs/node/pull/31112
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Ruben Bridgewater committed 2020-01-10 09:11:50 +01:00
1 parent d449c505f3
commit b52bf60518
5 files changed
+90 -28

No files matched your search

+2 -4
View File
@@ -718,7 +718,7 @@ Interface.prototype._historyNext = function() {
};
Interface.prototype._historyPrev = function() {
if (this.historyIndex < this.history.length) {
if (this.historyIndex < this.history.length && this.history.length) {
const search = this[kSubstringSearch] || '';
let index = this.historyIndex + 1;
while (index < this.history.length &&
@@ -727,9 +727,7 @@ Interface.prototype._historyPrev = function() {
index++;
}
if (index === this.history.length) {
// TODO(BridgeAR): Change this to:
// this.line = search;
return;
this.line = search;
} else {
this.line = this.history[index];
}
+14 -6
View File
@@ -482,12 +482,20 @@ function isWarned(emitter) {
fi.emit('keypress', '.', { name: 'up' }); // 'baz'
assert.strictEqual(rli.historyIndex, 2);
assert.strictEqual(rli.line, 'baz');
fi.emit('keypress', '.', { name: 'up' }); // 'baz'
assert.strictEqual(rli.historyIndex, 2);
assert.strictEqual(rli.line, 'baz');
fi.emit('keypress', '.', { name: 'up' }); // 'baz'
assert.strictEqual(rli.historyIndex, 2);
assert.strictEqual(rli.line, 'baz');
fi.emit('keypress', '.', { name: 'up' }); // 'ba'
assert.strictEqual(rli.historyIndex, 4);
assert.strictEqual(rli.line, 'ba');
fi.emit('keypress', '.', { name: 'up' }); // 'ba'
assert.strictEqual(rli.historyIndex, 4);
assert.strictEqual(rli.line, 'ba');
// Deactivate substring history search and reset history index.
fi.emit('keypress', '.', { name: 'right' }); // 'ba'
assert.strictEqual(rli.historyIndex, -1);
assert.strictEqual(rli.line, 'ba');
// Substring history search activated.
fi.emit('keypress', '.', { name: 'up' }); // 'ba'
assert.strictEqual(rli.historyIndex, 0);
assert.strictEqual(rli.line, 'bat');
rli.close();
}
@@ -78,8 +78,7 @@ const tests = [
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
checkTotal: true,
test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN],
test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN, DOWN],
expected: [prompt,
`${prompt}Array(100).fill(1).map((e, i) => i ** 2)`,
prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' +
@@ -92,6 +91,8 @@ const tests = [
`${prompt}555 + 909`,
prev && '\n// 1464',
`${prompt}let ab = 45`,
prompt,
`${prompt}let ab = 45`,
`${prompt}555 + 909`,
prev && '\n// 1464',
`${prompt}{key : {key2 :[] }}`,
@@ -138,9 +139,12 @@ const tests = [
// UP - skipping const foo = true
'\x1B[1G', '\x1B[0J',
'> 555 + 909', '\x1B[12G',
// UP, UP, ENTER. UPs at the end of the history have no effect.
'\r\n',
'1464\n',
// UP, UP
// UPs at the end of the history reset the line to the original input.
'\x1B[1G', '\x1B[0J',
'> 55', '\x1B[5G',
// ENTER
'\r\n', '55\n',
'\x1B[1G', '\x1B[0J',
'> ', '\x1B[3G',
'\r\n'
+34 -7
View File
@@ -10,6 +10,7 @@ const assert = require('assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const util = require('util');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
@@ -51,6 +52,7 @@ ActionStream.prototype.readable = true;
// Mock keys
const UP = { name: 'up' };
const DOWN = { name: 'down' };
const ENTER = { name: 'enter' };
const CLEAR = { ctrl: true, name: 'u' };
@@ -90,20 +92,42 @@ const tests = [
},
{
env: {},
test: [UP, '\'42\'', ENTER],
expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt],
test: [UP, '21', ENTER, "'42'", ENTER],
expected: [
prompt,
'2', '1', '21\n', prompt, prompt,
"'", '4', '2', "'", "'42'\n", prompt, prompt
],
clean: false
},
{ // Requires the above test case
env: {},
test: [UP, UP, ENTER],
expected: [prompt, `${prompt}'42'`, '\'42\'\n', prompt]
test: [UP, UP, CLEAR, ENTER, DOWN, CLEAR, ENTER, UP, ENTER],
expected: [
prompt,
`${prompt}'42'`,
`${prompt}21`,
prompt,
prompt,
`${prompt}'42'`,
prompt,
prompt,
`${prompt}21`,
'21\n',
prompt,
]
},
{
env: { NODE_REPL_HISTORY: historyPath,
NODE_REPL_HISTORY_SIZE: 1 },
test: [UP, UP, CLEAR],
expected: [prompt, `${prompt}'you look fabulous today'`, prompt]
test: [UP, UP, DOWN, CLEAR],
expected: [
prompt,
`${prompt}'you look fabulous today'`,
prompt,
`${prompt}'you look fabulous today'`,
prompt
]
},
{
env: { NODE_REPL_HISTORY: historyPathFail,
@@ -169,6 +193,8 @@ function runTest(assertCleaned) {
const opts = tests.shift();
if (!opts) return; // All done
console.log('NEW');
if (assertCleaned) {
try {
assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), '');
@@ -193,6 +219,7 @@ function runTest(assertCleaned) {
output: new stream.Writable({
write(chunk, _, next) {
const output = chunk.toString();
console.log('INPUT', util.inspect(output));
// Ignore escapes and blank lines
if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output))
@@ -207,7 +234,7 @@ function runTest(assertCleaned) {
next();
}
}),
prompt: prompt,
prompt,
useColors: false,
terminal: true
}, function(err, repl) {
@@ -8,6 +8,7 @@ const assert = require('assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const util = require('util');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
@@ -49,6 +50,7 @@ ActionStream.prototype.readable = true;
// Mock keys
const UP = { name: 'up' };
const DOWN = { name: 'down' };
const ENTER = { name: 'enter' };
const CLEAR = { ctrl: true, name: 'u' };
@@ -88,20 +90,40 @@ const tests = [
},
{
env: {},
test: [UP, '\'42\'', ENTER],
expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt],
test: [UP, '21', ENTER, "'42'", ENTER],
expected: [
prompt,
// TODO(BridgeAR): The line is refreshed too many times. The double prompt
// is redundant and can be optimized away.
'2', '1', '21\n', prompt, prompt,
"'", '4', '2', "'", "'42'\n", prompt, prompt
],
clean: false
},
{ // Requires the above test case
env: {},
test: [UP, UP, ENTER],
expected: [prompt, `${prompt}'42'`, '\'42\'\n', prompt]
test: [UP, UP, UP, DOWN, ENTER],
expected: [
prompt,
`${prompt}'42'`,
`${prompt}21`,
prompt,
`${prompt}21`,
'21\n',
prompt
]
},
{
env: { NODE_REPL_HISTORY: historyPath,
NODE_REPL_HISTORY_SIZE: 1 },
test: [UP, UP, CLEAR],
expected: [prompt, `${prompt}'you look fabulous today'`, prompt]
test: [UP, UP, DOWN, CLEAR],
expected: [
prompt,
`${prompt}'you look fabulous today'`,
prompt,
`${prompt}'you look fabulous today'`,
prompt
]
},
{
env: { NODE_REPL_HISTORY: historyPathFail,
@@ -167,6 +189,8 @@ function runTest(assertCleaned) {
const opts = tests.shift();
if (!opts) return; // All done
console.log('NEW');
if (assertCleaned) {
try {
assert.strictEqual(fs.readFileSync(defaultHistoryPath, 'utf8'), '');
@@ -192,6 +216,7 @@ function runTest(assertCleaned) {
output: new stream.Writable({
write(chunk, _, next) {
const output = chunk.toString();
console.log('INPUT', util.inspect(output));
// Ignore escapes and blank lines
if (output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output))