-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
repl: Support for eager evaluation #22875
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 1 commit
700c94e
c7b4b10
6e77bfe
5710523
b3908e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,6 +154,7 @@ function Interface(input, output, completer, terminal) { | |
| this.output = output; | ||
| this.input = input; | ||
| this.historySize = historySize; | ||
| this.previewResult = ''; | ||
| this.removeHistoryDuplicates = !!removeHistoryDuplicates; | ||
| this.crlfDelay = crlfDelay ? | ||
| MathMax(kMincrlfDelay, crlfDelay) : kMincrlfDelay; | ||
|
|
@@ -490,6 +491,42 @@ 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); | ||
| }; | ||
|
|
||
| // Append eager eval result to the | ||
| // Current line | ||
| Interface.prototype._appendPreview = function(result) { | ||
|
Member
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. 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.
Contributor
Author
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. Agreed, I will move them to |
||
| this.previewResult = `\u001b[90m // ${result}\u001b[39m`; | ||
|
antsmartian marked this conversation as resolved.
Outdated
|
||
| const line = `${this._prompt}${this.line} //${result}`; | ||
| const columns = this.output.columns; | ||
| const hasColors = this.output.hasColors(); | ||
| const s = hasColors ? | ||
| `${this._prompt}${this.line}${this.previewResult}` : line; | ||
|
Member
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 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 And is it correct that
Contributor
Author
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. Hmmm, I'm planning to send the output to this function and then use |
||
|
|
||
| // Cursor to left edge. | ||
| cursorTo(this.output, 0); | ||
| clearScreenDown(this.output); | ||
|
|
||
| if (columns !== undefined) { | ||
| this.output.write(line.length < columns ? | ||
| s : `${s.slice(0, columns - 3) | ||
| .replace(/\r?\n|\r/g, '')}...\u001b[39m`); | ||
|
Member
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 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.
Contributor
Author
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. 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.
Member
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. We already use 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.
Contributor
Author
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. @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 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 |
||
| } else { | ||
| this.output.write(s); | ||
| } | ||
|
|
||
| // Move back the cursor to the original position | ||
| cursorTo(this.output, this.cursor + this._prompt.length); | ||
| }; | ||
|
|
||
| // Refresh the line if preview present | ||
| Interface.prototype._clearPreview = function() { | ||
| if (this.previewResult !== '') { | ||
| this._refreshLine(); | ||
| this.previewResult = ''; | ||
| } | ||
| }; | ||
|
|
||
| Interface.prototype._tabComplete = function(lastKeypressWasTab) { | ||
|
|
@@ -629,6 +666,7 @@ Interface.prototype._deleteLeft = function() { | |
|
|
||
| this.cursor -= charSize; | ||
| this._refreshLine(); | ||
| this.emit('buffer', this.line); | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -640,6 +678,7 @@ Interface.prototype._deleteRight = function() { | |
| this.line = this.line.slice(0, this.cursor) + | ||
| this.line.slice(this.cursor + charSize, this.line.length); | ||
| this._refreshLine(); | ||
| this.emit('buffer', this.line); | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -655,6 +694,7 @@ Interface.prototype._deleteWordLeft = function() { | |
| this.line = leading + this.line.slice(this.cursor, this.line.length); | ||
| this.cursor = leading.length; | ||
| this._refreshLine(); | ||
| this.emit('buffer', this.line); | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -666,6 +706,7 @@ Interface.prototype._deleteWordRight = function() { | |
| this.line = this.line.slice(0, this.cursor) + | ||
| trailing.slice(match[0].length); | ||
| this._refreshLine(); | ||
| this.emit('buffer', this.line); | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -674,12 +715,14 @@ Interface.prototype._deleteLineLeft = function() { | |
| this.line = this.line.slice(this.cursor); | ||
| this.cursor = 0; | ||
| this._refreshLine(); | ||
| this.emit('buffer', this.line); | ||
| }; | ||
|
|
||
|
|
||
| Interface.prototype._deleteLineRight = function() { | ||
| this.line = this.line.slice(0, this.cursor); | ||
| this._refreshLine(); | ||
| this.emit('buffer', this.line); | ||
| }; | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,6 +111,9 @@ const { | |
|
|
||
| const history = require('internal/repl/history'); | ||
| const { setImmediate } = require('timers'); | ||
| const inspector = require('inspector'); | ||
| const kPreviewResults = Symbol('preview-result-fn'); | ||
| const util = require('util'); | ||
|
|
||
| // Lazy-loaded. | ||
| let processTopLevelAwait; | ||
|
|
@@ -265,6 +268,66 @@ function REPLServer(prompt, | |
|
|
||
| const self = this; | ||
|
|
||
| self[kPreviewResults] = (eagerSession, eagerEvalContextId) => { | ||
|
lundibundi marked this conversation as resolved.
Outdated
|
||
| this.on('buffer', (line) => { | ||
| Interface.prototype._clearPreview.call(this); | ||
|
|
||
| // No need of preview for a multiline statement | ||
|
lundibundi marked this conversation as resolved.
Outdated
|
||
| if (this[kBufferedCommandSymbol] !== '') | ||
| return; | ||
|
|
||
| eagerSession.post('Runtime.evaluate', { | ||
| expression: line.toString(), | ||
| generatePreview: true, | ||
| throwOnSideEffect: true, | ||
| timeout: 500, | ||
| executionContextId: eagerEvalContextId | ||
| }, (error, previewResult) => { | ||
|
|
||
| if (error) { | ||
| debug(`Error while generating preview ${error}`); | ||
| return; | ||
| } | ||
|
|
||
| if (undefined !== previewResult.result.value) { | ||
| const value = util.inspect(previewResult.result.value); | ||
| Interface.prototype._appendPreview.call(this, value); | ||
| return; | ||
| } | ||
|
|
||
|
|
||
| // If no exception and we have objectId | ||
| // Run the expression via callFunctionOn | ||
| // And return it from util inspect. | ||
|
Member
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. Nit: // If there was no exception and we've got an objectId
// stringify it with `util.inspect` via Runtime.callFunctionOn. |
||
| if (!previewResult.exceptionDetails && previewResult.result.objectId) { | ||
| eagerSession.post('Runtime.callFunctionOn', { | ||
| functionDeclaration: | ||
| 'function(arg) { return util.inspect(arg) }', | ||
| arguments: [previewResult.result], | ||
| executionContextId: eagerEvalContextId, | ||
| returnByValue: true, | ||
| }, (err, result) => { | ||
| if (!err) { | ||
|
lundibundi marked this conversation as resolved.
Outdated
|
||
| Interface.prototype._appendPreview | ||
| .call(this, result.result.value); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| }; | ||
|
|
||
|
|
||
| // Set up session for eager evaluation | ||
| const eagerSession = new inspector.Session(); | ||
| eagerSession.connect(); | ||
| // eslint-disable-next-line | ||
|
lundibundi marked this conversation as resolved.
Outdated
|
||
| eagerSession.once('Runtime.executionContextCreated', ({ params: { context } }) => { | ||
| self[kPreviewResults](eagerSession, context.id); | ||
| eagerSession.post('Runtime.disable'); | ||
| }); | ||
| eagerSession.post('Runtime.enable'); | ||
|
|
||
| // Pause taking in new input, and store the keys in a buffer. | ||
| const pausedBuffer = []; | ||
| let paused = false; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| 'use strict'; | ||
|
|
||
| // Flags: --expose-internals | ||
|
|
||
| const common = require('../common'); | ||
| const stream = require('stream'); | ||
| const REPL = require('internal/repl'); | ||
| const assert = require('assert'); | ||
|
|
||
| // Create an input stream specialized for testing an array of actions | ||
| class ActionStream extends stream.Stream { | ||
| run(data) { | ||
| const _iter = data[Symbol.iterator](); | ||
| const doAction = () => { | ||
| const next = _iter.next(); | ||
| if (next.done) { | ||
| // Close the repl. Note that it must have a clean prompt to do so. | ||
| setImmediate(() => { | ||
| this.emit('keypress', '', { ctrl: true, name: 'd' }); | ||
| }); | ||
| return; | ||
| } | ||
| const action = next.value; | ||
|
|
||
| if (typeof action === 'object') { | ||
| this.emit('keypress', '', action); | ||
| } else { | ||
| this.emit('data', `${action}\n`); | ||
| } | ||
| setImmediate(doAction); | ||
| }; | ||
| setImmediate(doAction); | ||
| } | ||
| resume() {} | ||
| pause() {} | ||
| } | ||
| ActionStream.prototype.readable = true; | ||
|
|
||
|
|
||
| // Mock keys | ||
| const ENTER = { name: 'enter' }; | ||
| const CLEAR = { ctrl: true, name: 'u' }; | ||
|
|
||
| const prompt = '> '; | ||
|
|
||
|
|
||
| const wrapWithColorCode = (code, result) => { | ||
| return `${prompt}${code}\u001b[90m // ${result}\u001b[39m`; | ||
| }; | ||
| const tests = [ | ||
| { | ||
| env: {}, | ||
| test: ['\' t\'.trim()', CLEAR], | ||
| expected: [wrapWithColorCode('\' t\'', '\' t\''), | ||
| wrapWithColorCode('\' t\'.trim', '[Function: trim]'), | ||
| wrapWithColorCode('\' t\'.trim()', '\'t\'')] | ||
| }, | ||
| { | ||
| env: {}, | ||
| test: ['3+5', CLEAR], | ||
| expected: [wrapWithColorCode('3', '3'), | ||
| wrapWithColorCode('3+5', '8')] | ||
| }, | ||
| { | ||
| env: {}, | ||
| test: ['[9,0].sort()', CLEAR], | ||
| expected: [wrapWithColorCode('[9,0]', '[ 9, 0 ]'), | ||
| wrapWithColorCode('[9,0].sort', '[Function: sort]'), | ||
| wrapWithColorCode('[9,0].sort()', '[ 0, 9 ]')] | ||
| }, | ||
| { | ||
| env: {}, | ||
| test: ['const obj = { m : () => {}}', ENTER, | ||
| 'obj.m', CLEAR], | ||
| expected: [ | ||
| wrapWithColorCode('obj', '{ m: [Function: m] }'), | ||
| wrapWithColorCode('obj.m', '[Function: m]')] | ||
| }, | ||
| { | ||
| env: {}, | ||
| test: ['const aObj = { a : { b : { c : [ {} , \'test\' ]}}}', ENTER, | ||
| 'aObj.a', CLEAR], | ||
| expected: [ | ||
| wrapWithColorCode('aObj', | ||
| '{ a: { b: { c: [ {}, \'test\' ] } } }'), | ||
| wrapWithColorCode('aObj.a', | ||
| '{ b: { c: [ {}, \'test\' ] } }')] | ||
| } | ||
| ]; | ||
| const numtests = tests.length; | ||
|
|
||
| const runTestWrap = common.mustCall(runTest, numtests); | ||
|
|
||
| function runTest() { | ||
| const opts = tests.shift(); | ||
| if (!opts) return; // All done | ||
|
|
||
| const env = opts.env; | ||
| const test = opts.test; | ||
| const expected = opts.expected; | ||
|
|
||
| REPL.createInternalRepl(env, { | ||
| input: new ActionStream(), | ||
| output: new stream.Writable({ | ||
| write(chunk, _, next) { | ||
| const output = chunk.toString(); | ||
|
|
||
| // Ignore everything except eval result | ||
| if (!output.includes('//')) { | ||
| return next(); | ||
| } | ||
|
|
||
| const toBeAsserted = expected[0]; | ||
| try { | ||
| assert.strictEqual(output, toBeAsserted); | ||
| expected.shift(); | ||
| } catch (err) { | ||
| console.error(`Failed test # ${numtests - tests.length}`); | ||
| throw err; | ||
| } | ||
|
|
||
| next(); | ||
| } | ||
| }), | ||
| prompt: prompt, | ||
| useColors: false, | ||
| terminal: true | ||
| }, function(err, repl) { | ||
| if (err) { | ||
| console.error(`Failed test # ${numtests - tests.length}`); | ||
| throw err; | ||
| } | ||
|
|
||
| repl.once('close', () => { | ||
| try { | ||
| // Ensure everything that we expected was output | ||
| assert.strictEqual(expected.length, 0); | ||
| setImmediate(runTestWrap, true); | ||
| } catch (err) { | ||
| console.error(`Failed test # ${numtests - tests.length}`); | ||
| throw err; | ||
| } | ||
| }); | ||
|
|
||
| repl.inputStream.run(test); | ||
| }); | ||
| } | ||
|
|
||
| // run the tests | ||
| runTest(); |

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: