-
Notifications
You must be signed in to change notification settings - Fork 29.7k
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
repl: Add editor mode support #7275
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thefourtheye There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thefourtheye |
||
// 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like I am totally missing something here. Why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thefourtheye Without it, > let obj = { member: "Hi", access() { return this.member } }
undefined
> obj.access()
'Hi'
> newAccess = obj.access
[Function: access]
> newAccess()
undefined
> |
||
self._ttyWrite = (d, key) => { | ||
if (!self.editorMode || !self.terminal) { | ||
ttyWrite(d, key); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like an infinite recursive call to me. Please help me understand this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I simply overlooked the fact that the original function is retained. This clears up a lot of things in my mind. Thanks. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are these keys the user can use while in editor mode? if so, there's no documentation for it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @trevnorris Yes but its not a new feature and is not specific to |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the comment is not necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thefourtheye It helps user to remember ^D for finish/save and ^C for cancel the input! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant the double slash at the beginning There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
> .editor
Entering editor mode (^D to finish, ^C to cancel)
Math.sqrt(3)
^D
1.7320508075688772
> vs > .editor
// Entering editor mode (^D to finish, ^C to cancel)
Math.sqrt(3)
^D
1.7320508075688772
> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ya, it looks nice. Lets have it then. |
||
} | ||
}); | ||
} | ||
|
||
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?