Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4875aa2

Browse filesBrowse files
princejwesleycjihrig
authored andcommitted
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: #7275 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Evan Lucas <evanlucas@me.com>
1 parent b20518a commit 4875aa2
Copy full SHA for 4875aa2

File tree

Expand file treeCollapse file tree

4 files changed

+206
-3
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

4 files changed

+206
-3
lines changed
Open diff view settings
Collapse file

‎doc/api/repl.md‎

Copy file name to clipboardExpand all lines: doc/api/repl.md
+15Lines changed: 15 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ The following special commands are supported by all REPL instances:
3838
`> .save ./file/to/save.js`
3939
* `.load` - Load a file into the current REPL session.
4040
`> .load ./file/to/load.js`
41+
* `.editor` - Enter editor mode (`<ctrl>-D` to finish, `<ctrl>-C` to cancel)
42+
43+
```js
44+
> .editor
45+
// Entering editor mode (^D to finish, ^C to cancel)
46+
function welcome(name) {
47+
return `Hello ${name}!`;
48+
}
49+
50+
welcome('Node.js User');
51+
52+
// ^D
53+
'Hello Node.js User!'
54+
>
55+
```
4156

4257
The following key combinations in the REPL have these special effects:
4358

Collapse file

‎lib/repl.js‎

Copy file name to clipboardExpand all lines: lib/repl.js
+114-3Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ function REPLServer(prompt,
223223
self.underscoreAssigned = false;
224224
self.last = undefined;
225225
self.breakEvalOnSigint = !!breakEvalOnSigint;
226+
self.editorMode = false;
226227

227228
self._inTemplateLiteral = false;
228229

@@ -394,7 +395,12 @@ function REPLServer(prompt,
394395
// Figure out which "complete" function to use.
395396
self.completer = (typeof options.completer === 'function')
396397
? options.completer
397-
: complete;
398+
: completer;
399+
400+
function completer(text, cb) {
401+
complete.call(self, text, self.editorMode
402+
? self.completeOnEditorMode(cb) : cb);
403+
}
398404

399405
Interface.call(this, {
400406
input: self.inputStream,
@@ -428,9 +434,11 @@ function REPLServer(prompt,
428434
});
429435

430436
var sawSIGINT = false;
437+
var sawCtrlD = false;
431438
self.on('SIGINT', function() {
432439
var empty = self.line.length === 0;
433440
self.clearLine();
441+
self.turnOffEditorMode();
434442

435443
if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {
436444
if (sawSIGINT) {
@@ -454,6 +462,11 @@ function REPLServer(prompt,
454462
debug('line %j', cmd);
455463
sawSIGINT = false;
456464

465+
if (self.editorMode) {
466+
self.bufferedCommand += cmd + '\n';
467+
return;
468+
}
469+
457470
// leading whitespaces in template literals should not be trimmed.
458471
if (self._inTemplateLiteral) {
459472
self._inTemplateLiteral = false;
@@ -499,7 +512,8 @@ function REPLServer(prompt,
499512

500513
// If error was SyntaxError and not JSON.parse error
501514
if (e) {
502-
if (e instanceof Recoverable && !self.lineParser.shouldFail) {
515+
if (e instanceof Recoverable && !self.lineParser.shouldFail &&
516+
!sawCtrlD) {
503517
// Start buffering data like that:
504518
// {
505519
// ... x: 1
@@ -515,6 +529,7 @@ function REPLServer(prompt,
515529
// Clear buffer if no SyntaxErrors
516530
self.lineParser.reset();
517531
self.bufferedCommand = '';
532+
sawCtrlD = false;
518533

519534
// If we got any output - print it (if no error)
520535
if (!e &&
@@ -555,9 +570,55 @@ function REPLServer(prompt,
555570
});
556571

557572
self.on('SIGCONT', function() {
558-
self.displayPrompt(true);
573+
if (self.editorMode) {
574+
self.outputStream.write(`${self._initialPrompt}.editor\n`);
575+
self.outputStream.write(
576+
'// Entering editor mode (^D to finish, ^C to cancel)\n');
577+
self.outputStream.write(`${self.bufferedCommand}\n`);
578+
self.prompt(true);
579+
} else {
580+
self.displayPrompt(true);
581+
}
559582
});
560583

584+
// Wrap readline tty to enable editor mode
585+
const ttyWrite = self._ttyWrite.bind(self);
586+
self._ttyWrite = (d, key) => {
587+
if (!self.editorMode || !self.terminal) {
588+
ttyWrite(d, key);
589+
return;
590+
}
591+
592+
// editor mode
593+
if (key.ctrl && !key.shift) {
594+
switch (key.name) {
595+
case 'd': // End editor mode
596+
self.turnOffEditorMode();
597+
sawCtrlD = true;
598+
ttyWrite(d, { name: 'return' });
599+
break;
600+
case 'n': // Override next history item
601+
case 'p': // Override previous history item
602+
break;
603+
default:
604+
ttyWrite(d, key);
605+
}
606+
} else {
607+
switch (key.name) {
608+
case 'up': // Override previous history item
609+
case 'down': // Override next history item
610+
break;
611+
case 'tab':
612+
// prevent double tab behavior
613+
self._previousKey = null;
614+
ttyWrite(d, key);
615+
break;
616+
default:
617+
ttyWrite(d, key);
618+
}
619+
}
620+
};
621+
561622
self.displayPrompt();
562623
}
563624
inherits(REPLServer, Interface);
@@ -680,6 +741,12 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
680741
REPLServer.super_.prototype.setPrompt.call(this, prompt);
681742
};
682743

744+
REPLServer.prototype.turnOffEditorMode = function() {
745+
this.editorMode = false;
746+
this.setPrompt(this._initialPrompt);
747+
};
748+
749+
683750
// A stream to push an array into a REPL
684751
// used in REPLServer.complete
685752
function ArrayStream() {
@@ -987,6 +1054,39 @@ function complete(line, callback) {
9871054
}
9881055
}
9891056

1057+
function longestCommonPrefix(arr = []) {
1058+
const cnt = arr.length;
1059+
if (cnt === 0) return '';
1060+
if (cnt === 1) return arr[0];
1061+
1062+
const first = arr[0];
1063+
// complexity: O(m * n)
1064+
for (let m = 0; m < first.length; m++) {
1065+
const c = first[m];
1066+
for (let n = 1; n < cnt; n++) {
1067+
const entry = arr[n];
1068+
if (m >= entry.length || c !== entry[m]) {
1069+
return first.substring(0, m);
1070+
}
1071+
}
1072+
}
1073+
return first;
1074+
}
1075+
1076+
REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
1077+
if (err) return callback(err);
1078+
1079+
const [completions, completeOn = ''] = results;
1080+
const prefixLength = completeOn.length;
1081+
1082+
if (prefixLength === 0) return callback(null, [[], completeOn]);
1083+
1084+
const isNotEmpty = (v) => v.length > 0;
1085+
const trimCompleteOnPrefix = (v) => v.substring(prefixLength);
1086+
const data = completions.filter(isNotEmpty).map(trimCompleteOnPrefix);
1087+
1088+
callback(null, [[`${completeOn}${longestCommonPrefix(data)}`], completeOn]);
1089+
};
9901090

9911091
/**
9921092
* Used to parse and execute the Node REPL commands.
@@ -1189,6 +1289,17 @@ function defineDefaultCommands(repl) {
11891289
this.displayPrompt();
11901290
}
11911291
});
1292+
1293+
repl.defineCommand('editor', {
1294+
help: 'Entering editor mode (^D to finish, ^C to cancel)',
1295+
action() {
1296+
if (!this.terminal) return;
1297+
this.editorMode = true;
1298+
REPLServer.super_.prototype.setPrompt.call(this, '');
1299+
this.outputStream.write(
1300+
'// Entering editor mode (^D to finish, ^C to cancel)\n');
1301+
}
1302+
});
11921303
}
11931304

11941305
function regexpEscape(s) {
Collapse file

‎test/parallel/test-repl-.editor.js‎

Copy file name to clipboard
+55Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const repl = require('repl');
6+
7+
// \u001b[1G - Moves the cursor to 1st column
8+
// \u001b[0J - Clear screen
9+
// \u001b[3G - Moves the cursor to 3rd column
10+
const terminalCode = '\u001b[1G\u001b[0J> \u001b[3G';
11+
12+
function run(input, output, event) {
13+
const stream = new common.ArrayStream();
14+
let found = '';
15+
16+
stream.write = (msg) => found += msg.replace('\r', '');
17+
18+
const expected = `${terminalCode}.editor\n` +
19+
'// Entering editor mode (^D to finish, ^C to cancel)\n' +
20+
`${input}${output}\n${terminalCode}`;
21+
22+
const replServer = repl.start({
23+
prompt: '> ',
24+
terminal: true,
25+
input: stream,
26+
output: stream,
27+
useColors: false
28+
});
29+
30+
stream.emit('data', '.editor\n');
31+
stream.emit('data', input);
32+
replServer.write('', event);
33+
replServer.close();
34+
assert.strictEqual(found, expected);
35+
}
36+
37+
const tests = [
38+
{
39+
input: '',
40+
output: '\n(To exit, press ^C again or type .exit)',
41+
event: {ctrl: true, name: 'c'}
42+
},
43+
{
44+
input: 'var i = 1;',
45+
output: '',
46+
event: {ctrl: true, name: 'c'}
47+
},
48+
{
49+
input: 'var i = 1;\ni + 3',
50+
output: '\n4',
51+
event: {ctrl: true, name: 'd'}
52+
}
53+
];
54+
55+
tests.forEach(({input, output, event}) => run(input, output, event));
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-repl-tab-complete.js
+22Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,25 @@ testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) => {
348348
'a'
349349
]);
350350
}));
351+
352+
// tab completion in editor mode
353+
const editorStream = new common.ArrayStream();
354+
const editor = repl.start({
355+
stream: editorStream,
356+
terminal: true,
357+
useColors: false
358+
});
359+
360+
editorStream.run(['.clear']);
361+
editorStream.run(['.editor']);
362+
363+
editor.completer('co', common.mustCall((error, data) => {
364+
assert.deepStrictEqual(data, [['con'], 'co']);
365+
}));
366+
367+
editorStream.run(['.clear']);
368+
editorStream.run(['.editor']);
369+
370+
editor.completer('var log = console.l', common.mustCall((error, data) => {
371+
assert.deepStrictEqual(data, [['console.log'], 'console.l']);
372+
}));

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.