diff options
Diffstat (limited to 'lib/internal/repl/utils.js')
-rw-r--r-- | lib/internal/repl/utils.js | 168 |
1 files changed, 165 insertions, 3 deletions
diff --git a/lib/internal/repl/utils.js b/lib/internal/repl/utils.js index 6c011e8533..c4280c1d1f 100644 --- a/lib/internal/repl/utils.js +++ b/lib/internal/repl/utils.js @@ -1,10 +1,12 @@ 'use strict'; const { + MathMin, Symbol, } = primordials; -const acorn = require('internal/deps/acorn/acorn/dist/acorn'); +const { tokTypes: tt, Parser: AcornParser } = + require('internal/deps/acorn/acorn/dist/acorn'); const privateMethods = require('internal/deps/acorn-plugins/acorn-private-methods/index'); const classFields = @@ -13,7 +15,30 @@ const numericSeparator = require('internal/deps/acorn-plugins/acorn-numeric-separator/index'); const staticClassFeatures = require('internal/deps/acorn-plugins/acorn-static-class-features/index'); -const { tokTypes: tt, Parser: AcornParser } = acorn; + +const { sendInspectorCommand } = require('internal/util/inspector'); + +const { + ERR_INSPECTOR_NOT_AVAILABLE +} = require('internal/errors').codes; + +const { + clearLine, + cursorTo, + moveCursor, +} = require('readline'); + +const { inspect } = require('util'); + +const debug = require('internal/util/debuglog').debuglog('repl'); + +const inspectOptions = { + depth: 1, + colors: false, + compact: true, + breakLength: Infinity +}; +const inspectedOptions = inspect(inspectOptions, { colors: false }); // If the error is that we've unexpectedly ended the input, // then let the user try to recover by adding more input. @@ -91,7 +116,144 @@ function isRecoverableError(e, code) { } } +function setupPreview(repl, contextSymbol, bufferSymbol, active) { + // Simple terminals can't handle previews. + if (process.env.TERM === 'dumb' || !active) { + return { showInputPreview() {}, clearPreview() {} }; + } + + let preview = null; + let lastPreview = ''; + + const clearPreview = () => { + if (preview !== null) { + moveCursor(repl.output, 0, 1); + clearLine(repl.output); + moveCursor(repl.output, 0, -1); + lastPreview = preview; + preview = null; + } + }; + + // This returns a code preview for arbitrary input code. + function getPreviewInput(input, callback) { + // For similar reasons as `defaultEval`, wrap expressions starting with a + // curly brace with parenthesis. + if (input.startsWith('{') && !input.endsWith(';')) { + input = `(${input})`; + } + sendInspectorCommand((session) => { + session.post('Runtime.evaluate', { + expression: input, + throwOnSideEffect: true, + timeout: 333, + contextId: repl[contextSymbol], + }, (error, preview) => { + if (error) { + callback(error); + return; + } + const { result } = preview; + if (result.value !== undefined) { + callback(null, inspect(result.value, inspectOptions)); + // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear + // where they came from and if they are recoverable or not. Other errors + // may be inspected. + } else if (preview.exceptionDetails && + (result.className === 'EvalError' || + result.className === 'SyntaxError' || + result.className === 'ReferenceError')) { + callback(null, null); + } else if (result.objectId) { + session.post('Runtime.callFunctionOn', { + functionDeclaration: `(v) => util.inspect(v, ${inspectedOptions})`, + objectId: result.objectId, + arguments: [result] + }, (error, preview) => { + if (error) { + callback(error); + } else { + callback(null, preview.result.value); + } + }); + } else { + // Either not serializable or undefined. + callback(null, result.unserializableValue || result.type); + } + }); + }, () => callback(new ERR_INSPECTOR_NOT_AVAILABLE())); + } + + const showInputPreview = () => { + // Prevent duplicated previews after a refresh. + if (preview !== null) { + return; + } + + const line = repl.line.trim(); + + // Do not preview if the command is buffered or if the line is empty. + if (repl[bufferSymbol] || line === '') { + return; + } + + getPreviewInput(line, (error, inspected) => { + // Ignore the output if the value is identical to the current line and the + // former preview is not identical to this preview. + if ((line === inspected && lastPreview !== inspected) || + inspected === null) { + return; + } + if (error) { + debug('Error while generating preview', error); + return; + } + // Do not preview `undefined` if colors are deactivated or explicitly + // requested. + if (inspected === 'undefined' && + (!repl.useColors || repl.ignoreUndefined)) { + return; + } + + preview = inspected; + + // Limit the output to maximum 250 characters. Otherwise it becomes a) + // difficult to read and b) non terminal REPLs would visualize the whole + // output. + const maxColumns = MathMin(repl.columns, 250); + + if (inspected.length > maxColumns) { + inspected = `${inspected.slice(0, maxColumns - 6)}...`; + } + const lineBreakPos = inspected.indexOf('\n'); + if (lineBreakPos !== -1) { + inspected = `${inspected.slice(0, lineBreakPos)}`; + } + const result = repl.useColors ? + `\u001b[90m${inspected}\u001b[39m` : + `// ${inspected}`; + + repl.output.write(`\n${result}`); + moveCursor(repl.output, 0, -1); + cursorTo(repl.output, repl.cursor + repl._prompt.length); + }); + }; + + // Refresh prints the whole screen again and the preview will be removed + // during that procedure. Print the preview again. This also makes sure + // the preview is always correct after resizing the terminal window. + const tmpRefresh = repl._refreshLine.bind(repl); + repl._refreshLine = () => { + preview = null; + tmpRefresh(); + showInputPreview(); + }; + + return { showInputPreview, clearPreview }; +} + module.exports = { isRecoverableError, - kStandaloneREPL: Symbol('kStandaloneREPL') + kStandaloneREPL: Symbol('kStandaloneREPL'), + setupPreview }; |