From 9eb96a14904430b1cadc0772177a5bf2c5118702 Mon Sep 17 00:00:00 2001 From: Prince J Wesley Date: Sun, 12 Jun 2016 13:37:33 +0530 Subject: [PATCH 1/6] 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 > ``` --- doc/api/repl.md | 1 + lib/repl.js | 112 +++++++++++++++++++++++- test/parallel/test-repl-.editor.js | 55 ++++++++++++ test/parallel/test-repl-tab-complete.js | 22 +++++ 4 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-repl-.editor.js diff --git a/doc/api/repl.md b/doc/api/repl.md index 5499f4dacb5462..9060fdfc6ee6c9 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -38,6 +38,7 @@ 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 (`-D` to finish, `-C` to cancel) The following key combinations in the REPL have these special effects: diff --git a/lib/repl.js b/lib/repl.js index 6c2352c4b46da9..9fd3b3277081d2 100644 --- a/lib/repl.js +++ b/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,50 @@ 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; + default: + ttyWrite(d, key); + } + } + }; + self.displayPrompt(); } inherits(REPLServer, Interface); @@ -680,6 +736,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 +1049,39 @@ function complete(line, callback) { } } +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]); + + 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; + } +}; /** * Used to parse and execute the Node REPL commands. @@ -1189,6 +1284,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) { diff --git a/test/parallel/test-repl-.editor.js b/test/parallel/test-repl-.editor.js new file mode 100644 index 00000000000000..6680261bfdb758 --- /dev/null +++ b/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(found === expected, `Expected: ${expected}, Found: ${found}`); +} + +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)); diff --git a/test/parallel/test-repl-tab-complete.js b/test/parallel/test-repl-tab-complete.js index 73a3fd148bf524..4ff4371875c8c2 100644 --- a/test/parallel/test-repl-tab-complete.js +++ b/test/parallel/test-repl-tab-complete.js @@ -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']); +})); From ac1f9a209418bd6d57565daae350d205d5317ed0 Mon Sep 17 00:00:00 2001 From: Prince J Wesley Date: Wed, 13 Jul 2016 08:47:20 +0530 Subject: [PATCH 2/6] completeOn can be undefined --- lib/repl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/repl.js b/lib/repl.js index 9fd3b3277081d2..76e1b18a542685 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1052,7 +1052,7 @@ function complete(line, callback) { REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => { if (err) return callback(err); - const [completions, completeOn] = results; + const [completions, completeOn = ''] = results; const prefixLength = completeOn.length; if (prefixLength === 0) return callback(null, [[], completeOn]); From 23e584215856b23324909564edcdcb809fa229af Mon Sep 17 00:00:00 2001 From: Prince J Wesley Date: Thu, 14 Jul 2016 00:04:06 +0530 Subject: [PATCH 3/6] Add example for editor mode --- doc/api/repl.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/api/repl.md b/doc/api/repl.md index 9060fdfc6ee6c9..2d361c9c115500 100644 --- a/doc/api/repl.md +++ b/doc/api/repl.md @@ -40,6 +40,20 @@ The following special commands are supported by all REPL instances: `> .load ./file/to/load.js` * `.editor` - Enter editor mode (`-D` to finish, `-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: * `-C` - When pressed once, has the same effect as the `.break` command. From 233ecc555a989b5dbf8f74ecef935356554edc14 Mon Sep 17 00:00:00 2001 From: Prince J Wesley Date: Mon, 1 Aug 2016 21:09:20 +0530 Subject: [PATCH 4/6] Move longestCommonPrefix outside tab complete --- lib/repl.js | 38 +++++++++++++++--------------- test/parallel/test-repl-.editor.js | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index 76e1b18a542685..aca7d41d7c2dd9 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1049,6 +1049,25 @@ 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); @@ -1062,25 +1081,6 @@ REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => { const data = completions.filter(isNotEmpty).map(trimCompleteOnPrefix); callback(null, [[`${completeOn}${longestCommonPrefix(data)}`], completeOn]); - - 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; - } }; /** diff --git a/test/parallel/test-repl-.editor.js b/test/parallel/test-repl-.editor.js index 6680261bfdb758..15765ad517d72a 100644 --- a/test/parallel/test-repl-.editor.js +++ b/test/parallel/test-repl-.editor.js @@ -31,7 +31,7 @@ function run(input, output, event) { stream.emit('data', input); replServer.write('', event); replServer.close(); - assert(found === expected, `Expected: ${expected}, Found: ${found}`); + assert.strictEqual(found, expected); } const tests = [ From 4134ddd8ea5c3a5e748e3b6073087814fba77ee6 Mon Sep 17 00:00:00 2001 From: Prince J Wesley Date: Tue, 2 Aug 2016 22:24:08 +0530 Subject: [PATCH 5/6] Prevent double tab behavior --- lib/repl.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/repl.js b/lib/repl.js index aca7d41d7c2dd9..b64ee0f5fa8885 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -608,6 +608,9 @@ function REPLServer(prompt, case 'up': // Override previous history item case 'down': // Override next history item break; + case 'tab': + // prevent double tab behavior + self._previousKey = null; default: ttyWrite(d, key); } From 370616c51366561f2d7fbd5e98672ac85fc9d44a Mon Sep 17 00:00:00 2001 From: Prince J Wesley Date: Wed, 3 Aug 2016 22:47:53 +0530 Subject: [PATCH 6/6] Fix linter issue --- lib/repl.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/repl.js b/lib/repl.js index b64ee0f5fa8885..293b98b6075d62 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -611,6 +611,8 @@ function REPLServer(prompt, case 'tab': // prevent double tab behavior self._previousKey = null; + ttyWrite(d, key); + break; default: ttyWrite(d, key); }