Skip to content

Commit

Permalink
repl: Add editor mode support
Browse files Browse the repository at this point in the history
```js
> node
> .editor
// Entering editor mode (^D to finish, ^C to cancel)
function test() {
  console.log('tested!');
}

test();

// ^D
tested!
undefined
>
```

PR-URL: #7275
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Evan Lucas <[email protected]>
  • Loading branch information
princejwesley committed Aug 6, 2016
1 parent 769f63c commit b779eb4
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 3 deletions.
15 changes: 15 additions & 0 deletions doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
117 changes: 114 additions & 3 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ function REPLServer(prompt,
self.underscoreAssigned = false;
self.last = undefined;
self.breakEvalOnSigint = !!breakEvalOnSigint;
self.editorMode = false;

self._inTemplateLiteral = false;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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 &&
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
55 changes: 55 additions & 0 deletions test/parallel/test-repl-.editor.js
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions test/parallel/test-repl-tab-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}));

0 comments on commit b779eb4

Please sign in to comment.