Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ result. Input and output may be from `stdin` and `stdout`, respectively, or may
be connected to any Node.js [stream][].

Instances of [`repl.REPLServer`][] support automatic completion of inputs,
completion preview, simplistic Emacs-style line editing, multi-line inputs,
completion preview, inline syntax highlighting, simplistic Emacs-style line
editing, multi-line inputs,
[ZSH][]-like reverse-i-search, [ZSH][]-like substring-based history search,
ANSI-styled output, saving and restoring current REPL session state, error
recovery, and customizable evaluation functions. Terminals that do not support
Expand Down Expand Up @@ -232,6 +233,27 @@ undefined
undefined
```

### Syntax highlighting

<!-- YAML
added: REPLACEME
-->

When `useColors` is `true`, the REPL highlights
JavaScript input using the following color scheme:

* Keywords (`const`, `let`, `function`, `if`, `return`, etc.): Magenta
* Strings (single-quoted, double-quoted, and template literals): Green
* Numbers: Yellow
* Boolean literals (`true`, `false`), `null`, `undefined`, `NaN`,
`Infinity`: Yellow
* Regular expressions: Red
* Comments (line and block): Gray

Syntax highlighting is applied only to the display; the internal `line`
property remains plain text. Highlighting can be disabled by passing
`syntaxHighlighting: false` to [`repl.start()`][].

### Reverse-i-search

<!-- YAML
Expand Down Expand Up @@ -747,6 +769,11 @@ changes:
previews or not. **Default:** `true` with the default eval function and
`false` in case a custom eval function is used. If `terminal` is falsy, then
there are no previews and the value of `preview` has no effect.
* `syntaxHighlighting` {boolean} If `true`, enables inline syntax
highlighting of REPL input using ANSI color codes. Keywords are displayed
in magenta, strings in green, numbers in yellow, regular expressions in
red, and comments in gray. Requires `useColors` to be `true` and `terminal` to be `true`.
**Default:** `true`.
* `handleError` {Function} This function customizes error handling in the REPL.
It receives the thrown exception as its first argument and must return one
of the following values synchronously:
Expand Down
92 changes: 92 additions & 0 deletions lib/internal/repl/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const {
const {
getStringWidth,
inspect,
stylizeWithColor,
} = require('internal/util/inspect');

const CJSModule = require('internal/modules/cjs/loader').Module;
Expand Down Expand Up @@ -143,6 +144,96 @@ function isRecoverableError(e, code) {
}
}

/**
* Maps an Acorn token to a util.inspect style name.
* Returns undefined for tokens that should not be highlighted.
* @param {object} token The Acorn token.
* @returns {string|undefined} The inspect style name.
*/
function tokenStyle(token) {
const { label, keyword } = token.type;

if (keyword !== undefined) {
if (label === 'true' || label === 'false') return 'boolean';
if (label === 'null') return 'null';
if (label === 'import' || label === 'export') return 'module';
return 'special';
}

switch (label) {
case 'name':
switch (token.value) {
case 'undefined':
return 'undefined';
case 'NaN':
case 'Infinity':
return 'number';
default:
return undefined;
}
case 'num':
return 'number';
case 'bigint':
return 'bigint';
case 'string':
case 'template':
case '`':
return 'string';
case 'regexp':
return 'regexp';
default:
return undefined;
}
}

/**
* Applies ANSI syntax highlighting to a JavaScript code string.
* Uses the Acorn tokenizer and util.inspect.styles for colors,
* so the color scheme stays in sync with util.inspect output.
* @param {string} code The JavaScript code string to highlight.
* @returns {string} The highlighted code string.
*/
function syntaxHighlight(code) {
if (code.length === 0) return code;

const { Parser } =
require('internal/deps/acorn/acorn/dist/acorn');
const tokenize = FunctionPrototypeBind(Parser.tokenizer, Parser);

let result = '';
let offset = 0;

function write(start, end, inspectStyle) {
result +=
StringPrototypeSlice(code, offset, start) +
stylizeWithColor(StringPrototypeSlice(code, start, end), inspectStyle);
offset = end;
}

try {
const iterator = tokenize(code, {
__proto__: null,
allowHashBang: true,
ecmaVersion: 'latest',
onComment(_block, _text, start, end) {
write(start, end, 'undefined');
},
});

for (const token of iterator) {
const inspectStyle = tokenStyle(token);
if (inspectStyle !== undefined) {
write(token.start, token.end, inspectStyle);
}
}
} catch {
// Acorn throws for unfinished strings, templates, and comments.
// Tokens reported before the error are still useful while typing.
}

return result + StringPrototypeSlice(code, offset);
}

function setupPreview(repl, contextSymbol, bufferSymbol, active) {
// Simple terminals can't handle previews.
if (process.env.TERM === 'dumb' || !active) {
Expand Down Expand Up @@ -866,6 +957,7 @@ module.exports = {
REPL_MODE_STRICT,
isRecoverableError,
kStandaloneREPL: Symbol('kStandaloneREPL'),
syntaxHighlight,
setupPreview,
setupReverseSearch,
isObjectLiteral,
Expand Down
6 changes: 6 additions & 0 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const {
kStandaloneREPL,
setupPreview,
setupReverseSearch,
syntaxHighlight,
kContextId,
getREPLResourceName,
getGlobalBuiltins,
Expand Down Expand Up @@ -239,13 +240,18 @@ class REPLServer extends Interface {
const preview = options.terminal &&
(options.preview !== undefined ? !!options.preview : !eval_);

const syntaxHighlightingEnabled = options.terminal &&
(options.syntaxHighlighting !== undefined ?
!!options.syntaxHighlighting : true);

super({
input: options.input,
output: options.output,
completer: options.completer || completer,
terminal: options.terminal,
historySize: options.historySize,
prompt,
colorize: syntaxHighlightingEnabled ? syntaxHighlight : undefined,
});

ObjectDefineProperty(this, 'inputStream', {
Expand Down
Loading
Loading