repl: Add editor mode support · nodejs/node@b779eb4
@@ -223,6 +223,7 @@ function REPLServer(prompt,
223223self.underscoreAssigned = false;
224224self.last = undefined;
225225self.breakEvalOnSigint = !!breakEvalOnSigint;
226+self.editorMode = false;
226227227228self._inTemplateLiteral = false;
228229@@ -394,7 +395,12 @@ function REPLServer(prompt,
394395// Figure out which "complete" function to use.
395396self.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+}
398404399405Interface.call(this, {
400406input: self.inputStream,
@@ -428,9 +434,11 @@ function REPLServer(prompt,
428434});
429435430436var sawSIGINT = false;
437+var sawCtrlD = false;
431438self.on('SIGINT', function() {
432439var empty = self.line.length === 0;
433440self.clearLine();
441+self.turnOffEditorMode();
434442435443if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {
436444if (sawSIGINT) {
@@ -454,6 +462,11 @@ function REPLServer(prompt,
454462debug('line %j', cmd);
455463sawSIGINT = false;
456464465+if (self.editorMode) {
466+self.bufferedCommand += cmd + '\n';
467+return;
468+}
469+457470// leading whitespaces in template literals should not be trimmed.
458471if (self._inTemplateLiteral) {
459472self._inTemplateLiteral = false;
@@ -499,7 +512,8 @@ function REPLServer(prompt,
499512500513// If error was SyntaxError and not JSON.parse error
501514if (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
516530self.lineParser.reset();
517531self.bufferedCommand = '';
532+sawCtrlD = false;
518533519534// If we got any output - print it (if no error)
520535if (!e &&
@@ -555,9 +570,55 @@ function REPLServer(prompt,
555570});
556571557572self.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});
560583584+// 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+561622self.displayPrompt();
562623}
563624inherits(REPLServer, Interface);
@@ -680,6 +741,12 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
680741REPLServer.super_.prototype.setPrompt.call(this, prompt);
681742};
682743744+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
685752function ArrayStream() {
@@ -987,6 +1054,39 @@ function complete(line, callback) {
9871054}
9881055}
98910561057+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+};
99010909911091/**
9921092 * Used to parse and execute the Node REPL commands.
@@ -1189,6 +1289,17 @@ function defineDefaultCommands(repl) {
11891289this.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}
1193130411941305function regexpEscape(s) {