Skip to content

Commit

Permalink
repl: support for eager evaluation
Browse files Browse the repository at this point in the history
  • Loading branch information
antsmartian committed Nov 26, 2019
1 parent a2dfa3c commit 700c94e
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 2 deletions.
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
this.emit('buffer', this.line);
};

// Append eager eval result to the
// Current line
Interface.prototype._appendPreview = function(result) {
this.previewResult = `\u001b[90m // ${result}\u001b[39m`;
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;

// 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`);
} 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) => {
this.on('buffer', (line) => {
Interface.prototype._clearPreview.call(this);

// No need of preview for a multiline statement
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.
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) {
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
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

0 comments on commit 700c94e

Please sign in to comment.