diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index d60b33208883d4..c14531d92a8e42 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -10,6 +10,8 @@ const numericSeparator = const staticClassFeatures = require('internal/deps/acorn-plugins/acorn-static-class-features/index'); const { tokTypes: tt, Parser: AcornParser } = acorn; +const util = require('util'); +const debug = require('internal/util/debuglog').debuglog('repl'); // If the error is that we've unexpectedly ended the input, // then let the user try to recover by adding more input. @@ -87,7 +89,98 @@ function isRecoverableError(e, code) { } } +// Appends the preview of the result +// to the tty. +function appendPreview(repl, result, cursorTo, clearScreenDown) { + repl.previewResult = `\u001b[90m // ${result}\u001b[39m`; + const line = `${repl._prompt}${repl.line} //${result}`; + const columns = repl.output.columns; + const hasColors = repl.output.hasColors ? repl.output.hasColors() : false; + const s = hasColors ? + `${repl._prompt}${repl.line}${repl.previewResult}` : line; + + // Cursor to left edge. + cursorTo(repl.output, 0); + clearScreenDown(repl.output); + + if (columns !== undefined) { + repl.output.write(line.length < columns ? + s : `${s.slice(0, columns - 3) + .replace(/\r?\n|\r/g, '')}...\u001b[39m`); + } else { + repl.output.write(s); + } + + // Move back the cursor to the original position + cursorTo(repl.output, repl.cursor + repl._prompt.length); +} + +function clearPreview(repl) { + if (repl.previewResult !== '') { + repl._refreshLine(); + repl.previewResult = ''; + } +} + +// Called whenever a line changes +// in repl and the eager eval will be +// executed against the line using v8 session +let readline; +function makePreview(repl, eagerSession, eagerEvalContextId, line) { + const lazyReadline = () => { + if (!readline) readline = require('readline'); + return readline; + }; + + const { cursorTo, clearScreenDown } = lazyReadline(); + + clearPreview(repl); + + 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); + appendPreview(repl, value, cursorTo, clearScreenDown); + return; + } + + + // If there is no exception and we got + // objectId in the result, stringify it + // using 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) { + appendPreview(repl, result.result.value, + cursorTo, clearScreenDown); + } else { + debug('eager eval error', err); + } + }); + } + }); +} + + module.exports = { isRecoverableError, - kStandaloneREPL: Symbol('kStandaloneREPL') + kStandaloneREPL: Symbol('kStandaloneREPL'), + makePreview }; diff --git a/lib/readline.js b/lib/readline.js index c29aabb82b31a2..cfc1e83c5e3423 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -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,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 + this.emit('buffer', this.line); }; Interface.prototype._tabComplete = function(lastKeypressWasTab) { @@ -629,6 +632,7 @@ Interface.prototype._deleteLeft = function() { this.cursor -= charSize; this._refreshLine(); + this.emit('buffer', this.line); } }; @@ -640,6 +644,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 +660,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 +672,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 +681,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); }; diff --git a/lib/repl.js b/lib/repl.js index 8519eeffd6c520..70b6e5c84940f7 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -95,7 +95,8 @@ const experimentalREPLAwait = require('internal/options').getOptionValue( ); const { isRecoverableError, - kStandaloneREPL + kStandaloneREPL, + makePreview } = require('internal/repl/utils'); const { getOwnNonIndexProperties, @@ -111,6 +112,7 @@ const { const history = require('internal/repl/history'); const { setImmediate } = require('timers'); +const inspector = require('inspector'); // Lazy-loaded. let processTopLevelAwait; @@ -223,6 +225,20 @@ function REPLServer(prompt, throw new ERR_INVALID_REPL_EVAL_CONFIG(); } + const eagerSession = new inspector.Session(); + eagerSession.connect(); + eagerSession.once('Runtime.executionContextCreated', + ({ params: { context } }) => { + this.on('buffer', (line) => { + // No need of preview for a multiline statement. + if (this[kBufferedCommandSymbol] !== '') + return; + makePreview(self, eagerSession, context.id, line); + }); + eagerSession.post('Runtime.disable'); + }); + eagerSession.post('Runtime.enable'); + // Add this listener only once and use a WeakSet that contains the REPLs // domains. Otherwise we'd have to add a single listener to each REPL instance // and that could trigger the `MaxListenersExceededWarning`. diff --git a/test/parallel/test-repl-persistent-history.js b/test/parallel/test-repl-persistent-history.js index bb10085eccfcf6..4b9c51e38e881b 100644 --- a/test/parallel/test-repl-persistent-history.js +++ b/test/parallel/test-repl-persistent-history.js @@ -91,7 +91,9 @@ const tests = [ { env: {}, test: [UP, '\'42\'', ENTER], - expected: [prompt, '\'', '4', '2', '\'', '\'42\'\n', prompt, prompt], + expected: [prompt, '\'', '4', '2', '\'', + '> \'42\' //\'42\'', '\'42\'\n', + prompt, prompt], clean: false }, { // Requires the above test case diff --git a/test/parallel/test-repl-preview-result.js b/test/parallel/test-repl-preview-result.js new file mode 100644 index 00000000000000..ea1405084f95d4 --- /dev/null +++ b/test/parallel/test-repl-preview-result.js @@ -0,0 +1,152 @@ +'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: [Array] } } }'), + 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; + const ouput = 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(); + }, + }); + ouput.hasColors = () => true; + + REPL.createInternalRepl(env, { + input: new ActionStream(), + output: ouput, + 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(); diff --git a/test/parallel/test-repl-programmatic-history.js b/test/parallel/test-repl-programmatic-history.js index 7eda401a7c386b..c47478f09e43fa 100644 --- a/test/parallel/test-repl-programmatic-history.js +++ b/test/parallel/test-repl-programmatic-history.js @@ -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' //'42'`, + '\'42\'\n', prompt, prompt], clean: false }, { // Requires the above test case