Browse Source

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>
v6.x
Prince J Wesley 9 years ago
committed by cjihrig
parent
commit
4875aa2aa2
  1. 15
      doc/api/repl.md
  2. 117
      lib/repl.js
  3. 55
      test/parallel/test-repl-.editor.js
  4. 22
      test/parallel/test-repl-tab-complete.js

15
doc/api/repl.md

@ -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

@ -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

@ -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));

22
test/parallel/test-repl-tab-complete.js

@ -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…
Cancel
Save