Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
repl: support for eager evaluation
  • Loading branch information
antsmartian committed Nov 26, 2019
commit 700c94ed7992917b8e55f52ba6bd7f318b800531
43 changes: 43 additions & 0 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
// Emit current line for generating preview
// Emit current line for generating preview.

this.emit('buffer', this.line);
};

// Append eager eval result to the
// Current line
Interface.prototype._appendPreview = function(result) {
Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Contributor Author

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.

this.previewResult = `\u001b[90m // ${result}\u001b[39m`;
Comment thread
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;
Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Contributor Author

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.


// 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`);
Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Contributor Author

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:

Screen Shot 2019-11-27 at 11 36 47 AM

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? 🤔

} 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) {
Expand Down Expand Up @@ -629,6 +666,7 @@ Interface.prototype._deleteLeft = function() {

this.cursor -= charSize;
this._refreshLine();
this.emit('buffer', this.line);
}
};

Expand All @@ -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);
}
};

Expand All @@ -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);
}
};

Expand All @@ -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);
}
};

Expand All @@ -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);
};


Expand Down
63 changes: 63 additions & 0 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -265,6 +268,66 @@ function REPLServer(prompt,

const self = this;

self[kPreviewResults] = (eagerSession, eagerEvalContextId) => {
Comment thread
lundibundi marked this conversation as resolved.
Outdated
this.on('buffer', (line) => {
Interface.prototype._clearPreview.call(this);

// No need of preview for a multiline statement
Comment thread
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.
Copy link
Copy Markdown
Member

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.

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) {
Comment thread
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
Comment thread
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;
Expand Down
4 changes: 3 additions & 1 deletion test/parallel/test-repl-persistent-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ const tests = [
{
env: {},
test: [UP, '\'42\'', ENTER],
expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt],
expected: [prompt, '\'', '4', '2', '\'',
'> \'42\'\u001b[90m // \'42\'\u001b[39m', '\'42\'\n',
prompt, prompt],
clean: false
},
{ // Requires the above test case
Expand Down
150 changes: 150 additions & 0 deletions test/parallel/test-repl-preview-result.js
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();
4 changes: 3 additions & 1 deletion test/parallel/test-repl-programmatic-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ const tests = [
{
env: {},
test: [UP, '\'42\'', ENTER],
expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt],
expected: [prompt, '\'', '4', '2', '\'',
`${prompt}'42'\u001b[90m // '42'\u001b[39m`,
'\'42\'\n', prompt, prompt],
clean: false
},
{ // Requires the above test case
Expand Down