repl: Add editor mode support
```js > node > .editor // Entering editor mode (^D to finish, ^C to cancel) function test() { console.log('tested!'); } test(); // ^D tested! undefined > ``` PR-URL: https://github.com/nodejs/node/pull/7275 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Evan Lucas <evanlucas@me.com>
This commit is contained in:
parent
769f63ccd8
commit
b779eb423d
@ -38,6 +38,21 @@ The following special commands are supported by all REPL instances:
|
|||||||
`> .save ./file/to/save.js`
|
`> .save ./file/to/save.js`
|
||||||
* `.load` - Load a file into the current REPL session.
|
* `.load` - Load a file into the current REPL session.
|
||||||
`> .load ./file/to/load.js`
|
`> .load ./file/to/load.js`
|
||||||
|
* `.editor` - Enter editor mode (`<ctrl>-D` to finish, `<ctrl>-C` to cancel)
|
||||||
|
|
||||||
|
```js
|
||||||
|
> .editor
|
||||||
|
// Entering editor mode (^D to finish, ^C to cancel)
|
||||||
|
function welcome(name) {
|
||||||
|
return `Hello ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
welcome('Node.js User');
|
||||||
|
|
||||||
|
// ^D
|
||||||
|
'Hello Node.js User!'
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
The following key combinations in the REPL have these special effects:
|
The following key combinations in the REPL have these special effects:
|
||||||
|
|
||||||
|
117
lib/repl.js
117
lib/repl.js
@ -223,6 +223,7 @@ function REPLServer(prompt,
|
|||||||
self.underscoreAssigned = false;
|
self.underscoreAssigned = false;
|
||||||
self.last = undefined;
|
self.last = undefined;
|
||||||
self.breakEvalOnSigint = !!breakEvalOnSigint;
|
self.breakEvalOnSigint = !!breakEvalOnSigint;
|
||||||
|
self.editorMode = false;
|
||||||
|
|
||||||
self._inTemplateLiteral = false;
|
self._inTemplateLiteral = false;
|
||||||
|
|
||||||
@ -394,7 +395,12 @@ function REPLServer(prompt,
|
|||||||
// Figure out which "complete" function to use.
|
// Figure out which "complete" function to use.
|
||||||
self.completer = (typeof options.completer === 'function')
|
self.completer = (typeof options.completer === 'function')
|
||||||
? options.completer
|
? options.completer
|
||||||
: complete;
|
: completer;
|
||||||
|
|
||||||
|
function completer(text, cb) {
|
||||||
|
complete.call(self, text, self.editorMode
|
||||||
|
? self.completeOnEditorMode(cb) : cb);
|
||||||
|
}
|
||||||
|
|
||||||
Interface.call(this, {
|
Interface.call(this, {
|
||||||
input: self.inputStream,
|
input: self.inputStream,
|
||||||
@ -428,9 +434,11 @@ function REPLServer(prompt,
|
|||||||
});
|
});
|
||||||
|
|
||||||
var sawSIGINT = false;
|
var sawSIGINT = false;
|
||||||
|
var sawCtrlD = false;
|
||||||
self.on('SIGINT', function() {
|
self.on('SIGINT', function() {
|
||||||
var empty = self.line.length === 0;
|
var empty = self.line.length === 0;
|
||||||
self.clearLine();
|
self.clearLine();
|
||||||
|
self.turnOffEditorMode();
|
||||||
|
|
||||||
if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {
|
if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {
|
||||||
if (sawSIGINT) {
|
if (sawSIGINT) {
|
||||||
@ -454,6 +462,11 @@ function REPLServer(prompt,
|
|||||||
debug('line %j', cmd);
|
debug('line %j', cmd);
|
||||||
sawSIGINT = false;
|
sawSIGINT = false;
|
||||||
|
|
||||||
|
if (self.editorMode) {
|
||||||
|
self.bufferedCommand += cmd + '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// leading whitespaces in template literals should not be trimmed.
|
// leading whitespaces in template literals should not be trimmed.
|
||||||
if (self._inTemplateLiteral) {
|
if (self._inTemplateLiteral) {
|
||||||
self._inTemplateLiteral = false;
|
self._inTemplateLiteral = false;
|
||||||
@ -499,7 +512,8 @@ function REPLServer(prompt,
|
|||||||
|
|
||||||
// If error was SyntaxError and not JSON.parse error
|
// If error was SyntaxError and not JSON.parse error
|
||||||
if (e) {
|
if (e) {
|
||||||
if (e instanceof Recoverable && !self.lineParser.shouldFail) {
|
if (e instanceof Recoverable && !self.lineParser.shouldFail &&
|
||||||
|
!sawCtrlD) {
|
||||||
// Start buffering data like that:
|
// Start buffering data like that:
|
||||||
// {
|
// {
|
||||||
// ... x: 1
|
// ... x: 1
|
||||||
@ -515,6 +529,7 @@ function REPLServer(prompt,
|
|||||||
// Clear buffer if no SyntaxErrors
|
// Clear buffer if no SyntaxErrors
|
||||||
self.lineParser.reset();
|
self.lineParser.reset();
|
||||||
self.bufferedCommand = '';
|
self.bufferedCommand = '';
|
||||||
|
sawCtrlD = false;
|
||||||
|
|
||||||
// If we got any output - print it (if no error)
|
// If we got any output - print it (if no error)
|
||||||
if (!e &&
|
if (!e &&
|
||||||
@ -555,9 +570,55 @@ function REPLServer(prompt,
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.on('SIGCONT', function() {
|
self.on('SIGCONT', function() {
|
||||||
self.displayPrompt(true);
|
if (self.editorMode) {
|
||||||
|
self.outputStream.write(`${self._initialPrompt}.editor\n`);
|
||||||
|
self.outputStream.write(
|
||||||
|
'// Entering editor mode (^D to finish, ^C to cancel)\n');
|
||||||
|
self.outputStream.write(`${self.bufferedCommand}\n`);
|
||||||
|
self.prompt(true);
|
||||||
|
} else {
|
||||||
|
self.displayPrompt(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wrap readline tty to enable editor mode
|
||||||
|
const ttyWrite = self._ttyWrite.bind(self);
|
||||||
|
self._ttyWrite = (d, key) => {
|
||||||
|
if (!self.editorMode || !self.terminal) {
|
||||||
|
ttyWrite(d, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// editor mode
|
||||||
|
if (key.ctrl && !key.shift) {
|
||||||
|
switch (key.name) {
|
||||||
|
case 'd': // End editor mode
|
||||||
|
self.turnOffEditorMode();
|
||||||
|
sawCtrlD = true;
|
||||||
|
ttyWrite(d, { name: 'return' });
|
||||||
|
break;
|
||||||
|
case 'n': // Override next history item
|
||||||
|
case 'p': // Override previous history item
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ttyWrite(d, key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (key.name) {
|
||||||
|
case 'up': // Override previous history item
|
||||||
|
case 'down': // Override next history item
|
||||||
|
break;
|
||||||
|
case 'tab':
|
||||||
|
// prevent double tab behavior
|
||||||
|
self._previousKey = null;
|
||||||
|
ttyWrite(d, key);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ttyWrite(d, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.displayPrompt();
|
self.displayPrompt();
|
||||||
}
|
}
|
||||||
inherits(REPLServer, Interface);
|
inherits(REPLServer, Interface);
|
||||||
@ -680,6 +741,12 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
|
|||||||
REPLServer.super_.prototype.setPrompt.call(this, prompt);
|
REPLServer.super_.prototype.setPrompt.call(this, prompt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
REPLServer.prototype.turnOffEditorMode = function() {
|
||||||
|
this.editorMode = false;
|
||||||
|
this.setPrompt(this._initialPrompt);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// A stream to push an array into a REPL
|
// A stream to push an array into a REPL
|
||||||
// used in REPLServer.complete
|
// used in REPLServer.complete
|
||||||
function ArrayStream() {
|
function ArrayStream() {
|
||||||
@ -987,6 +1054,39 @@ function complete(line, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function longestCommonPrefix(arr = []) {
|
||||||
|
const cnt = arr.length;
|
||||||
|
if (cnt === 0) return '';
|
||||||
|
if (cnt === 1) return arr[0];
|
||||||
|
|
||||||
|
const first = arr[0];
|
||||||
|
// complexity: O(m * n)
|
||||||
|
for (let m = 0; m < first.length; m++) {
|
||||||
|
const c = first[m];
|
||||||
|
for (let n = 1; n < cnt; n++) {
|
||||||
|
const entry = arr[n];
|
||||||
|
if (m >= entry.length || c !== entry[m]) {
|
||||||
|
return first.substring(0, m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
|
||||||
|
if (err) return callback(err);
|
||||||
|
|
||||||
|
const [completions, completeOn = ''] = results;
|
||||||
|
const prefixLength = completeOn.length;
|
||||||
|
|
||||||
|
if (prefixLength === 0) return callback(null, [[], completeOn]);
|
||||||
|
|
||||||
|
const isNotEmpty = (v) => v.length > 0;
|
||||||
|
const trimCompleteOnPrefix = (v) => v.substring(prefixLength);
|
||||||
|
const data = completions.filter(isNotEmpty).map(trimCompleteOnPrefix);
|
||||||
|
|
||||||
|
callback(null, [[`${completeOn}${longestCommonPrefix(data)}`], completeOn]);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to parse and execute the Node REPL commands.
|
* Used to parse and execute the Node REPL commands.
|
||||||
@ -1189,6 +1289,17 @@ function defineDefaultCommands(repl) {
|
|||||||
this.displayPrompt();
|
this.displayPrompt();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
repl.defineCommand('editor', {
|
||||||
|
help: 'Entering editor mode (^D to finish, ^C to cancel)',
|
||||||
|
action() {
|
||||||
|
if (!this.terminal) return;
|
||||||
|
this.editorMode = true;
|
||||||
|
REPLServer.super_.prototype.setPrompt.call(this, '');
|
||||||
|
this.outputStream.write(
|
||||||
|
'// Entering editor mode (^D to finish, ^C to cancel)\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function regexpEscape(s) {
|
function regexpEscape(s) {
|
||||||
|
55
test/parallel/test-repl-.editor.js
Normal file
55
test/parallel/test-repl-.editor.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const repl = require('repl');
|
||||||
|
|
||||||
|
// \u001b[1G - Moves the cursor to 1st column
|
||||||
|
// \u001b[0J - Clear screen
|
||||||
|
// \u001b[3G - Moves the cursor to 3rd column
|
||||||
|
const terminalCode = '\u001b[1G\u001b[0J> \u001b[3G';
|
||||||
|
|
||||||
|
function run(input, output, event) {
|
||||||
|
const stream = new common.ArrayStream();
|
||||||
|
let found = '';
|
||||||
|
|
||||||
|
stream.write = (msg) => found += msg.replace('\r', '');
|
||||||
|
|
||||||
|
const expected = `${terminalCode}.editor\n` +
|
||||||
|
'// Entering editor mode (^D to finish, ^C to cancel)\n' +
|
||||||
|
`${input}${output}\n${terminalCode}`;
|
||||||
|
|
||||||
|
const replServer = repl.start({
|
||||||
|
prompt: '> ',
|
||||||
|
terminal: true,
|
||||||
|
input: stream,
|
||||||
|
output: stream,
|
||||||
|
useColors: false
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.emit('data', '.editor\n');
|
||||||
|
stream.emit('data', input);
|
||||||
|
replServer.write('', event);
|
||||||
|
replServer.close();
|
||||||
|
assert.strictEqual(found, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
input: '',
|
||||||
|
output: '\n(To exit, press ^C again or type .exit)',
|
||||||
|
event: {ctrl: true, name: 'c'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'var i = 1;',
|
||||||
|
output: '',
|
||||||
|
event: {ctrl: true, name: 'c'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'var i = 1;\ni + 3',
|
||||||
|
output: '\n4',
|
||||||
|
event: {ctrl: true, name: 'd'}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
tests.forEach(({input, output, event}) => run(input, output, event));
|
@ -348,3 +348,25 @@ testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) => {
|
|||||||
'a'
|
'a'
|
||||||
]);
|
]);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// tab completion in editor mode
|
||||||
|
const editorStream = new common.ArrayStream();
|
||||||
|
const editor = repl.start({
|
||||||
|
stream: editorStream,
|
||||||
|
terminal: true,
|
||||||
|
useColors: false
|
||||||
|
});
|
||||||
|
|
||||||
|
editorStream.run(['.clear']);
|
||||||
|
editorStream.run(['.editor']);
|
||||||
|
|
||||||
|
editor.completer('co', common.mustCall((error, data) => {
|
||||||
|
assert.deepStrictEqual(data, [['con'], 'co']);
|
||||||
|
}));
|
||||||
|
|
||||||
|
editorStream.run(['.clear']);
|
||||||
|
editorStream.run(['.editor']);
|
||||||
|
|
||||||
|
editor.completer('var log = console.l', common.mustCall((error, data) => {
|
||||||
|
assert.deepStrictEqual(data, [['console.log'], 'console.l']);
|
||||||
|
}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user