summaryrefslogtreecommitdiff
path: root/lib/internal/repl/utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/internal/repl/utils.js')
-rw-r--r--lib/internal/repl/utils.js168
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
};