-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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: Support for eager evaluation #22875
Conversation
@nodejs/repl |
any reason this is limited to call expressions? |
@devsnek I need to extend to other expressions as well, but just wanted to post my progress here so that I can get review from the community (mainly on whether or not I'm in right direction). Edit: Or we can first go with call expressions and expand there-after. As always, its all depends on what community says :) |
32d6067
to
047b6e6
Compare
@devsnek Added support for other expressions and as well for |
047b6e6
to
7138252
Compare
lib/repl.js
Outdated
} | ||
|
||
try { | ||
if (isValidExpression(line)) { |
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.
there's no need to check if its a valid expression, or even an expression at all.
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.
I just thought sending code for every key press for eval might be unnecessary.
Yes we can remove them, anyways the code block is in try catch block, so in worst case that should catch the exception if any.
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.
v8 will detect invalid code anyway, so there's no need to manually check.
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.
@devsnek Ok make sense, if I remove the check, then there would be preview results as necessary. Hence, few test cases will be failing (I could see test-repl-persistent-history.js
failing already). Let me work on fixing those test cases and update the PR.
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.
This pretty neat but certainly needs more work (as you seem to be aware)!
Some things I noticed:
- Not all edits to the current line cause the eager eval to re-appear.
- The eager eval should probably be colored with grey and the colors of inspect outputs should probably also go though a grey-ing filter.
@Fishrock123 Yes, you are correct. Not all edits (with valid code) trigger eager eval. I knew that it needs to be worked out. Regarding color will change them. Thanks for taking time to review this! |
Just to give an update, I will find some time later this week, afterwhich I will be updating the PR with comments addressed. Sorry for the delay :) |
bcadcce
to
e005596
Compare
@devsnek @Fishrock123 Addressed your comments. Now eager eval will work on all lines (when v8 says it has a preview). Also changed the color to grey. Updated the tests. Let me know if anything else is needed. Thanks! |
Friendly remainder : @devsnek @Fishrock123 🔉 |
lib/readline.js
Outdated
@@ -456,6 +458,28 @@ Interface.prototype._insertString = function(c) { | |||
|
|||
// a hack to get the line refreshed if it's needed | |||
this._moveCursor(0); | |||
// emit current line for generating preview | |||
this.emit('buffer', this.line); |
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.
This line is there in both the branches, at the end. Perhaps this can be moved out of the if..else
.
lib/repl.js
Outdated
}, (error, result) => { | ||
|
||
if (error) { | ||
// mostly possible side effect exception |
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.
Its better to expect the specific exceptions and ignore them. Is that possible?
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.
Also if there is an error, will the result
object always have result
property?
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.
Yes result
will always have result
even in case of error
. Will refactor the variable to previewResult
, so that its better readable.
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.
And about the exception, it can be syntax issues, side effects etc. Not sure if I can catch them specifically. @devsnek can give some thoughts on this.
|
||
assert.ok(actual[0].includes(wrapWithColorCode('\'test\''))); | ||
// side effect scenario, preview is not available | ||
assert.ok(!actual[2].includes('// ')); |
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.
appendPreview
doesn't have a space character after //
. Is this intentional?
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.
Nope, it was a miss. Good catch. Will fix them.
e005596
to
2d59a2e
Compare
@thefourtheye I have addressed your comments. @devsnek @jdalton @Fishrock123 Can you please have a look? |
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.
Getting there... also needs some more tests. :)
I'd really like to see this make it into a release though!
lib/readline.js
Outdated
}; | ||
|
||
Interface.prototype.appendPreview = function(result) { | ||
this.previewResult = `\u001b[90m //${result}\u001b[39m`; |
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.
this.previewResult = `\u001b[90m //${result}\u001b[39m`; | |
this.previewResult = `\u001b[90m // ${result}\u001b[39m`; |
lib/repl.js
Outdated
@@ -844,10 +844,49 @@ REPLServer.prototype.createContext = function() { | |||
}); | |||
|
|||
addBuiltinLibsToObject(context); | |||
previewResults.call(this); |
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.
This shouldn't be necessary - can you instead add the function on the class but either prefix the function name with _
or, better, put it behind a Symbol? Something like kPreviewResults = new Symbol('Preview Results Fn')
, and then you can do this[kPreviewResults]()
.
lib/repl.js
Outdated
|
||
return context; | ||
}; | ||
|
||
function previewResults() { |
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.
Do we really need this wrapping function? Also, it's confusing, since there is a variable with the same name.
lib/repl.js
Outdated
} | ||
|
||
if (undefined !== previewResult.result.value) { | ||
const value = util.inspect(previewResult.result.value); |
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.
Are you sure this is what you think it is?
It currently seems to be printing Objects as Arrays of values:
> process.versions
{ http_parser: '2.8.0',
node: '12.0.0-pre',
v8: '7.0.276.38-node.11',
uv: '1.24.0',
zlib: '1.2.11',
ares: '1.15.0',
modules: '67',
nghttp2: '1.34.0',
napi: '3',
openssl: '1.1.0i',
icu: '63.1',
unicode: '11.0',
cldr: '34.0',
tz: '2018e' }
> process.versions //[ '2.8.0', '12.0.0-pre', '7.0.276.38-node.11', '1.24.0', '1.2.11' ]
lib/repl.js
Outdated
|
||
if (previewResult.result.preview) { | ||
const previewResults = previewResult.result.preview.properties; | ||
const previewData = previewResults.map((preview) => preview.value); // eslint-disable-line max-len |
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.
I think this should avoid the eslint message:
const previewData = previewResults.map((preview) => preview.value); // eslint-disable-line max-len | |
const previewData = previewResults.map( | |
(preview) => preview.value | |
); |
lib/repl.js
Outdated
Interface.prototype.appendPreview.call(this, value); | ||
} | ||
}); | ||
}, () => []); |
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.
Did you mean this?
}, () => []); | |
}, () => {}); |
r.on('exit', common.mustCall(() => { | ||
const actual = output.split('\n'); | ||
|
||
const wrapWithColorCode = (str) => `\u001b[90m //${str}\u001b[39m`; |
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.
const wrapWithColorCode = (str) => `\u001b[90m //${str}\u001b[39m`; | |
const wrapWithColorCode = (str) => `\u001b[90m // ${str}\u001b[39m`; |
@Fishrock123 Thanks for your review, will update the thread once I'm done with your review comments. |
e48a315
to
0b0783f
Compare
@Fishrock123 Addressed review comments. I guess few things I need do to:
|
319ea89
to
87b14fc
Compare
@nodejs/repl PTAL.. Have added test cases and made changes requested by @Fishrock123 |
@BridgeAR will address your comments. |
@nodejs/repl @Fishrock123 @BridgeAR PTAL.. |
@antsmartian have you seen my comments? They still seem to apply? |
@BridgeAR Yes, I have addressed the comments and also added check for color part in the prev commit, may be you can have a look now. Thanks! |
lib/readline.js
Outdated
|
||
if (columns) { | ||
const colorDepth = this.output.getColorDepth(); | ||
const s = colorDepth > 256 ? |
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.
The maximum color depth is 24. You can also use hasColors()
instead of getColorDepth()
as it's more intuitive to use.
lib/readline.js
Outdated
const colorDepth = this.output.getColorDepth(); | ||
const s = colorDepth > 256 ? | ||
`${this._prompt}${this.line}${this.previewResult}` : | ||
`${this._prompt}${this.line}${result}`; |
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.
This is not identical to the color part (it does not only strip the colors).
lib/readline.js
Outdated
const s = colorDepth > 256 ? | ||
`${this._prompt}${this.line}${this.previewResult}` : | ||
`${this._prompt}${this.line}${result}`; | ||
this.output.write(s.length < columns ? |
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.
The length check includes the colors but that should ideally not be the case. Colors are not visible characters and should as such not be calculated towards the total length.
lib/readline.js
Outdated
`${this._prompt}${this.line}${result}`; | ||
this.output.write(s.length < columns ? | ||
s : `${s.slice(0, columns - 3) | ||
.replace(/\r?\n|\r/g, '')}...\u001b[39m`); |
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.
This adds the color unconditionally. Can you also please elaborate what exactly this does? I am not sure I understand why this part is sliced and replaced.
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.
The slice and replace part is mainly for splitting the new lines. Since in output terminal has only limited columns, we need to split them and replace with empty space so that we can show them the content in single line.
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.
We already use util.inspect()
on the preview. We should use options that deactivate the colors for the preview and make sure the output is printed on a single line. That can be done with the compact
options set to true
in combination with breakLength
set to Infinity
and colors
set to false
.
I would then limit the preview only instead of limiting the input as well. Right now we unconditionally slice the input value and that doesn't seem right.
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.
@BridgeAR: (sorry for a very late reply)
What you are saying is actually a good idea. But I was just trying to play with the options provided by you but still that doesn't seems to be atleast fixing the problem what we have now. For example, if the user types process
, we get a long string, so if we apply the options like below:
util.inspect(process, {breakLength: Infinity, compact: true})
still we are getting results like below:
This actually breaks the cursor position as the output is huge. I feel, the only way for us to slice the string to the length of the tty column
. May be is there any other way we could achieve it? 🤔
lib/readline.js
Outdated
|
||
// Append eager eval result to the | ||
// Current line | ||
Interface.prototype._appendPreview = function(result) { |
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.
It would be possible to add internal functionality in a separate file that would only be loaded internally. That way we do not have to pollute the prototype further. An underscore in a property name would not hinder anyone from using or monkey patching this function.
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.
Agreed, I will move them to internal/readline/util
.
lib/repl.js
Outdated
if (!previewResult.exceptionDetails && previewResult.result.objectId) { | ||
eagerSession.post('Runtime.callFunctionOn', { | ||
functionDeclaration: | ||
'function(arg) { return util.inspect(arg, {depth: Infinity}) }', |
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.
depth: Infinity
does not sound like a good default to me. The preview should ideally be short and on the same line. This could cause the terminal to add lots of output.
lib/readline.js
Outdated
s : `${s.slice(0, columns - 3) | ||
.replace(/\r?\n|\r/g, '')}...\u001b[39m`); | ||
} else { | ||
this.output.write(this._prompt + this.line + this.previewResult); |
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.
This adds colors unconditionally.
lib/readline.js
Outdated
const columns = this.output.columns; | ||
const hasColors = this.output.hasColors(); | ||
const s = hasColors ? | ||
`${this._prompt}${this.line}${this.previewResult}` : line; |
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.
I personally would keep the preview, even if no colors are supported. The comment should be indication enough.
If we do not want to have a preview in case the colors are deactivated, we should not start the eagerSession
in the REPL. That way no code is run unnecessary.
And is it correct that this._prompt
is necessary in one case but not in the other?
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.
Hmmm, I'm planning to send the output to this function and then use util.inspect
within the function to customize our output. Do you think that would be a better approach? Because anyways I need to call util.inspect
in my REPL session.
lib/readline.js
Outdated
`${this._prompt}${this.line}${result}`; | ||
this.output.write(s.length < columns ? | ||
s : `${s.slice(0, columns - 3) | ||
.replace(/\r?\n|\r/g, '')}...\u001b[39m`); |
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.
We already use util.inspect()
on the preview. We should use options that deactivate the colors for the preview and make sure the output is printed on a single line. That can be done with the compact
options set to true
in combination with breakLength
set to Infinity
and colors
set to false
.
I would then limit the preview only instead of limiting the input as well. Right now we unconditionally slice the input value and that doesn't seem right.
ping @antsmartian, will you be working on this further? |
@lundibundi Yes. This is open for long time ( I know :( ), I wanted it to get closed. Will look at it this week. Will update the PR soon. |
163f7fd
to
700c94e
Compare
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.
👍 just a few nits and code refactoring suggestions.
lib/repl.js
Outdated
// If no exception and we have objectId | ||
// Run the expression via callFunctionOn | ||
// And return it from util inspect. |
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.
Nit:
// If there was no exception and we've got an objectId
// stringify it with `util.inspect` via Runtime.callFunctionOn.
73af1b2
to
b3908e4
Compare
@lundibundi @nodejs/repl PTAL.. 🔉 |
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.
Mostly LGTM from me, but I'm not that familiar with repl code to put a ✔️. This will have to get approval from @BridgeAR or someone else from @nodejs/repl.
Great work, would love to see preview in the repl.
return readline; | ||
}; | ||
|
||
const { cursorTo, clearScreenDown } = lazyReadline(); |
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.
🤔 can we just const { cursorTo, clearScreenDown } = require('readline')
at the top now?
repl.output.write(s); | ||
} | ||
|
||
// Move back the cursor to the original position |
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.
Nit:
// Move back the cursor to the original position | |
// Move back the cursor to the original position. |
@@ -87,7 +89,98 @@ function isRecoverableError(e, code) { | |||
} | |||
} | |||
|
|||
// Appends the preview of the result | |||
// to the tty. | |||
function appendPreview(repl, result, cursorTo, clearScreenDown) { |
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.
function appendPreview(repl, result, cursorTo, clearScreenDown) { | |
function appendPreview(repl, preview, cursorTo, clearScreenDown) { |
maybe? To make it a little clearer.
// Appends the preview of the result | ||
// to the tty. |
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.
Nit: I think this will fit on one line.
@@ -490,6 +491,8 @@ Interface.prototype._insertString = function(c) { | |||
// A hack to get the line refreshed if it's needed | |||
this._moveCursor(0); | |||
} | |||
// Emit current line for generating preview |
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.
Nit:
// Emit current line for generating preview | |
// Emit current line for generating preview. |
return; | ||
} | ||
|
||
if (undefined !== previewResult.result.value) { |
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.
Nit:
if (undefined !== previewResult.result.value) { | |
if (previewResult.result.value !== undefined) { |
eagerSession.once('Runtime.executionContextCreated', | ||
({ params: { context } }) => { |
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.
Perhaps to avoid big indentation:
eagerSession.once(
'Runtime.executionContextCreated',
({ params: { context } }) => {
})
repl.output.write(line.length < columns ? | ||
s : `${s.slice(0, columns - 3) | ||
.replace(/\r?\n|\r/g, '')}...\u001b[39m`); |
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.
Nit:
repl.output.write(line.length < columns ?
s : s.slice(0, columns - 3).replace(/\r?\n|\r/g, '') + '...\u001b[39m');
This got superseded by #30811 |
Address the issue #20977
Well, I would say its "initial" checkins. May be work will be required for handling cursors, additionaledge cases (may be 🤔 ) etc, but I guess this should be a good starting point.Added code for handling cursors and also for line refresh, when necessary.
Thanks for your time on this.
make -j4 test
(UNIX), orvcbuild test
(Windows) passes