-
-
Notifications
You must be signed in to change notification settings - Fork 33.5k
repl: Add editor mode support #7275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9eb96a1
ac1f9a2
23e5842
233ecc5
4134ddd
370616c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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); | ||
|
@@ -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() { | ||
|
@@ -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. | ||
|
@@ -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) { | ||
|
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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe add a snippet of an example?