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`
|
||||
* `.load` - Load a file into the current REPL session.
|
||||
`> .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:
|
||||
|
||||
|
117
lib/repl.js
117
lib/repl.js
@ -223,6 +223,7 @@ function REPLServer(prompt,
|
||||
self.underscoreAssigned = false;
|
||||
self.last = undefined;
|
||||
self.breakEvalOnSigint = !!breakEvalOnSigint;
|
||||
self.editorMode = false;
|
||||
|
||||
self._inTemplateLiteral = false;
|
||||
|
||||
@ -394,7 +395,12 @@ function REPLServer(prompt,
|
||||
// Figure out which "complete" function to use.
|
||||
self.completer = (typeof options.completer === 'function')
|
||||
? options.completer
|
||||
: complete;
|
||||
: completer;
|
||||
|
||||
function completer(text, cb) {
|
||||
complete.call(self, text, self.editorMode
|
||||
? self.completeOnEditorMode(cb) : cb);
|
||||
}
|
||||
|
||||
Interface.call(this, {
|
||||
input: self.inputStream,
|
||||
@ -428,9 +434,11 @@ function REPLServer(prompt,
|
||||
});
|
||||
|
||||
var sawSIGINT = false;
|
||||
var sawCtrlD = false;
|
||||
self.on('SIGINT', function() {
|
||||
var empty = self.line.length === 0;
|
||||
self.clearLine();
|
||||
self.turnOffEditorMode();
|
||||
|
||||
if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {
|
||||
if (sawSIGINT) {
|
||||
@ -454,6 +462,11 @@ function REPLServer(prompt,
|
||||
debug('line %j', cmd);
|
||||
sawSIGINT = false;
|
||||
|
||||
if (self.editorMode) {
|
||||
self.bufferedCommand += cmd + '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// leading whitespaces in template literals should not be trimmed.
|
||||
if (self._inTemplateLiteral) {
|
||||
self._inTemplateLiteral = false;
|
||||
@ -499,7 +512,8 @@ function REPLServer(prompt,
|
||||
|
||||
// If error was SyntaxError and not JSON.parse error
|
||||
if (e) {
|
||||
if (e instanceof Recoverable && !self.lineParser.shouldFail) {
|
||||
if (e instanceof Recoverable && !self.lineParser.shouldFail &&
|
||||
!sawCtrlD) {
|
||||
// Start buffering data like that:
|
||||
// {
|
||||
// ... x: 1
|
||||
@ -515,6 +529,7 @@ function REPLServer(prompt,
|
||||
// Clear buffer if no SyntaxErrors
|
||||
self.lineParser.reset();
|
||||
self.bufferedCommand = '';
|
||||
sawCtrlD = false;
|
||||
|
||||
// If we got any output - print it (if no error)
|
||||
if (!e &&
|
||||
@ -555,9 +570,55 @@ function REPLServer(prompt,
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
inherits(REPLServer, Interface);
|
||||
@ -680,6 +741,12 @@ REPLServer.prototype.setPrompt = function setPrompt(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
|
||||
// used in REPLServer.complete
|
||||
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.
|
||||
@ -1189,6 +1289,17 @@ function defineDefaultCommands(repl) {
|
||||
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) {
|
||||
|
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'
|
||||
]);
|
||||
}));
|
||||
|
||||
// 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