Skip to content

Commit 700c94e

Browse files
committed
repl: support for eager evaluation
1 parent a2dfa3c commit 700c94e

File tree

5 files changed

+262
-2
lines changed

5 files changed

+262
-2
lines changed

lib/readline.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ function Interface(input, output, completer, terminal) {
154154
this.output = output;
155155
this.input = input;
156156
this.historySize = historySize;
157+
this.previewResult = '';
157158
this.removeHistoryDuplicates = !!removeHistoryDuplicates;
158159
this.crlfDelay = crlfDelay ?
159160
MathMax(kMincrlfDelay, crlfDelay) : kMincrlfDelay;
@@ -490,6 +491,42 @@ Interface.prototype._insertString = function(c) {
490491
// A hack to get the line refreshed if it's needed
491492
this._moveCursor(0);
492493
}
494+
// Emit current line for generating preview
495+
this.emit('buffer', this.line);
496+
};
497+
498+
// Append eager eval result to the
499+
// Current line
500+
Interface.prototype._appendPreview = function(result) {
501+
this.previewResult = `\u001b[90m // ${result}\u001b[39m`;
502+
const line = `${this._prompt}${this.line} //${result}`;
503+
const columns = this.output.columns;
504+
const hasColors = this.output.hasColors();
505+
const s = hasColors ?
506+
`${this._prompt}${this.line}${this.previewResult}` : line;
507+
508+
// Cursor to left edge.
509+
cursorTo(this.output, 0);
510+
clearScreenDown(this.output);
511+
512+
if (columns !== undefined) {
513+
this.output.write(line.length < columns ?
514+
s : `${s.slice(0, columns - 3)
515+
.replace(/\r?\n|\r/g, '')}...\u001b[39m`);
516+
} else {
517+
this.output.write(s);
518+
}
519+
520+
// Move back the cursor to the original position
521+
cursorTo(this.output, this.cursor + this._prompt.length);
522+
};
523+
524+
// Refresh the line if preview present
525+
Interface.prototype._clearPreview = function() {
526+
if (this.previewResult !== '') {
527+
this._refreshLine();
528+
this.previewResult = '';
529+
}
493530
};
494531

495532
Interface.prototype._tabComplete = function(lastKeypressWasTab) {
@@ -629,6 +666,7 @@ Interface.prototype._deleteLeft = function() {
629666

630667
this.cursor -= charSize;
631668
this._refreshLine();
669+
this.emit('buffer', this.line);
632670
}
633671
};
634672

@@ -640,6 +678,7 @@ Interface.prototype._deleteRight = function() {
640678
this.line = this.line.slice(0, this.cursor) +
641679
this.line.slice(this.cursor + charSize, this.line.length);
642680
this._refreshLine();
681+
this.emit('buffer', this.line);
643682
}
644683
};
645684

@@ -655,6 +694,7 @@ Interface.prototype._deleteWordLeft = function() {
655694
this.line = leading + this.line.slice(this.cursor, this.line.length);
656695
this.cursor = leading.length;
657696
this._refreshLine();
697+
this.emit('buffer', this.line);
658698
}
659699
};
660700

@@ -666,6 +706,7 @@ Interface.prototype._deleteWordRight = function() {
666706
this.line = this.line.slice(0, this.cursor) +
667707
trailing.slice(match[0].length);
668708
this._refreshLine();
709+
this.emit('buffer', this.line);
669710
}
670711
};
671712

@@ -674,12 +715,14 @@ Interface.prototype._deleteLineLeft = function() {
674715
this.line = this.line.slice(this.cursor);
675716
this.cursor = 0;
676717
this._refreshLine();
718+
this.emit('buffer', this.line);
677719
};
678720

679721

680722
Interface.prototype._deleteLineRight = function() {
681723
this.line = this.line.slice(0, this.cursor);
682724
this._refreshLine();
725+
this.emit('buffer', this.line);
683726
};
684727

685728

lib/repl.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ const {
111111

112112
const history = require('internal/repl/history');
113113
const { setImmediate } = require('timers');
114+
const inspector = require('inspector');
115+
const kPreviewResults = Symbol('preview-result-fn');
116+
const util = require('util');
114117

115118
// Lazy-loaded.
116119
let processTopLevelAwait;
@@ -265,6 +268,66 @@ function REPLServer(prompt,
265268

266269
const self = this;
267270

271+
self[kPreviewResults] = (eagerSession, eagerEvalContextId) => {
272+
this.on('buffer', (line) => {
273+
Interface.prototype._clearPreview.call(this);
274+
275+
// No need of preview for a multiline statement
276+
if (this[kBufferedCommandSymbol] !== '')
277+
return;
278+
279+
eagerSession.post('Runtime.evaluate', {
280+
expression: line.toString(),
281+
generatePreview: true,
282+
throwOnSideEffect: true,
283+
timeout: 500,
284+
executionContextId: eagerEvalContextId
285+
}, (error, previewResult) => {
286+
287+
if (error) {
288+
debug(`Error while generating preview ${error}`);
289+
return;
290+
}
291+
292+
if (undefined !== previewResult.result.value) {
293+
const value = util.inspect(previewResult.result.value);
294+
Interface.prototype._appendPreview.call(this, value);
295+
return;
296+
}
297+
298+
299+
// If no exception and we have objectId
300+
// Run the expression via callFunctionOn
301+
// And return it from util inspect.
302+
if (!previewResult.exceptionDetails && previewResult.result.objectId) {
303+
eagerSession.post('Runtime.callFunctionOn', {
304+
functionDeclaration:
305+
'function(arg) { return util.inspect(arg) }',
306+
arguments: [previewResult.result],
307+
executionContextId: eagerEvalContextId,
308+
returnByValue: true,
309+
}, (err, result) => {
310+
if (!err) {
311+
Interface.prototype._appendPreview
312+
.call(this, result.result.value);
313+
}
314+
});
315+
}
316+
});
317+
});
318+
};
319+
320+
321+
// Set up session for eager evaluation
322+
const eagerSession = new inspector.Session();
323+
eagerSession.connect();
324+
// eslint-disable-next-line
325+
eagerSession.once('Runtime.executionContextCreated', ({ params: { context } }) => {
326+
self[kPreviewResults](eagerSession, context.id);
327+
eagerSession.post('Runtime.disable');
328+
});
329+
eagerSession.post('Runtime.enable');
330+
268331
// Pause taking in new input, and store the keys in a buffer.
269332
const pausedBuffer = [];
270333
let paused = false;

test/parallel/test-repl-persistent-history.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ const tests = [
9191
{
9292
env: {},
9393
test: [UP, '\'42\'', ENTER],
94-
expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt],
94+
expected: [prompt, '\'', '4', '2', '\'',
95+
'> \'42\'\u001b[90m // \'42\'\u001b[39m', '\'42\'\n',
96+
prompt, prompt],
9597
clean: false
9698
},
9799
{ // Requires the above test case
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use strict';
2+
3+
// Flags: --expose-internals
4+
5+
const common = require('../common');
6+
const stream = require('stream');
7+
const REPL = require('internal/repl');
8+
const assert = require('assert');
9+
10+
// Create an input stream specialized for testing an array of actions
11+
class ActionStream extends stream.Stream {
12+
run(data) {
13+
const _iter = data[Symbol.iterator]();
14+
const doAction = () => {
15+
const next = _iter.next();
16+
if (next.done) {
17+
// Close the repl. Note that it must have a clean prompt to do so.
18+
setImmediate(() => {
19+
this.emit('keypress', '', { ctrl: true, name: 'd' });
20+
});
21+
return;
22+
}
23+
const action = next.value;
24+
25+
if (typeof action === 'object') {
26+
this.emit('keypress', '', action);
27+
} else {
28+
this.emit('data', `${action}\n`);
29+
}
30+
setImmediate(doAction);
31+
};
32+
setImmediate(doAction);
33+
}
34+
resume() {}
35+
pause() {}
36+
}
37+
ActionStream.prototype.readable = true;
38+
39+
40+
// Mock keys
41+
const ENTER = { name: 'enter' };
42+
const CLEAR = { ctrl: true, name: 'u' };
43+
44+
const prompt = '> ';
45+
46+
47+
const wrapWithColorCode = (code, result) => {
48+
return `${prompt}${code}\u001b[90m // ${result}\u001b[39m`;
49+
};
50+
const tests = [
51+
{
52+
env: {},
53+
test: ['\' t\'.trim()', CLEAR],
54+
expected: [wrapWithColorCode('\' t\'', '\' t\''),
55+
wrapWithColorCode('\' t\'.trim', '[Function: trim]'),
56+
wrapWithColorCode('\' t\'.trim()', '\'t\'')]
57+
},
58+
{
59+
env: {},
60+
test: ['3+5', CLEAR],
61+
expected: [wrapWithColorCode('3', '3'),
62+
wrapWithColorCode('3+5', '8')]
63+
},
64+
{
65+
env: {},
66+
test: ['[9,0].sort()', CLEAR],
67+
expected: [wrapWithColorCode('[9,0]', '[ 9, 0 ]'),
68+
wrapWithColorCode('[9,0].sort', '[Function: sort]'),
69+
wrapWithColorCode('[9,0].sort()', '[ 0, 9 ]')]
70+
},
71+
{
72+
env: {},
73+
test: ['const obj = { m : () => {}}', ENTER,
74+
'obj.m', CLEAR],
75+
expected: [
76+
wrapWithColorCode('obj', '{ m: [Function: m] }'),
77+
wrapWithColorCode('obj.m', '[Function: m]')]
78+
},
79+
{
80+
env: {},
81+
test: ['const aObj = { a : { b : { c : [ {} , \'test\' ]}}}', ENTER,
82+
'aObj.a', CLEAR],
83+
expected: [
84+
wrapWithColorCode('aObj',
85+
'{ a: { b: { c: [ {}, \'test\' ] } } }'),
86+
wrapWithColorCode('aObj.a',
87+
'{ b: { c: [ {}, \'test\' ] } }')]
88+
}
89+
];
90+
const numtests = tests.length;
91+
92+
const runTestWrap = common.mustCall(runTest, numtests);
93+
94+
function runTest() {
95+
const opts = tests.shift();
96+
if (!opts) return; // All done
97+
98+
const env = opts.env;
99+
const test = opts.test;
100+
const expected = opts.expected;
101+
102+
REPL.createInternalRepl(env, {
103+
input: new ActionStream(),
104+
output: new stream.Writable({
105+
write(chunk, _, next) {
106+
const output = chunk.toString();
107+
108+
// Ignore everything except eval result
109+
if (!output.includes('//')) {
110+
return next();
111+
}
112+
113+
const toBeAsserted = expected[0];
114+
try {
115+
assert.strictEqual(output, toBeAsserted);
116+
expected.shift();
117+
} catch (err) {
118+
console.error(`Failed test # ${numtests - tests.length}`);
119+
throw err;
120+
}
121+
122+
next();
123+
}
124+
}),
125+
prompt: prompt,
126+
useColors: false,
127+
terminal: true
128+
}, function(err, repl) {
129+
if (err) {
130+
console.error(`Failed test # ${numtests - tests.length}`);
131+
throw err;
132+
}
133+
134+
repl.once('close', () => {
135+
try {
136+
// Ensure everything that we expected was output
137+
assert.strictEqual(expected.length, 0);
138+
setImmediate(runTestWrap, true);
139+
} catch (err) {
140+
console.error(`Failed test # ${numtests - tests.length}`);
141+
throw err;
142+
}
143+
});
144+
145+
repl.inputStream.run(test);
146+
});
147+
}
148+
149+
// run the tests
150+
runTest();

test/parallel/test-repl-programmatic-history.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ const tests = [
8989
{
9090
env: {},
9191
test: [UP, '\'42\'', ENTER],
92-
expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt],
92+
expected: [prompt, '\'', '4', '2', '\'',
93+
`${prompt}'42'\u001b[90m // '42'\u001b[39m`,
94+
'\'42\'\n', prompt, prompt],
9395
clean: false
9496
},
9597
{ // Requires the above test case

0 commit comments

Comments
 (0)