Skip to content

Commit 756e16e

Browse files
committed
repl: add inline syntax highlighting
Use the Acorn tokenizer to colorize JavaScript input in real-time. Keywords are magenta, strings green, numbers yellow, regexps red, and comments gray. Highlighting is display-only — repl.line stays plain text. Can be disabled via `syntaxHighlighting: false` in repl.start() or TERM=dumb. Refs: #48164 Signed-off-by: hemanth <hemanth.hm@gmail.com>
1 parent c286592 commit 756e16e

4 files changed

Lines changed: 419 additions & 1 deletion

File tree

doc/api/repl.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ result. Input and output may be from `stdin` and `stdout`, respectively, or may
2727
be connected to any Node.js [stream][].
2828

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

235+
### Syntax highlighting
236+
237+
<!-- YAML
238+
added: REPLACEME
239+
-->
240+
241+
When `useColors` is `true`, the REPL highlights
242+
JavaScript input using the following color scheme:
243+
244+
* **Keywords** (`const`, `let`, `function`, `if`, `return`, etc.): Magenta
245+
* **Strings** (single-quoted, double-quoted, and template literals): Green
246+
* **Numbers**: Yellow
247+
* **Boolean literals** (`true`, `false`), `null`, `undefined`, `NaN`,
248+
`Infinity`: Yellow
249+
* **Regular expressions**: Red
250+
* **Comments** (line and block): Gray
251+
252+
Syntax highlighting is applied only to the display; the internal `line`
253+
property remains plain text. Highlighting can be disabled by passing
254+
`syntaxHighlighting: false` to [`repl.start()`][].
255+
235256
### Reverse-i-search
236257

237258
<!-- YAML
@@ -747,6 +768,11 @@ changes:
747768
previews or not. **Default:** `true` with the default eval function and
748769
`false` in case a custom eval function is used. If `terminal` is falsy, then
749770
there are no previews and the value of `preview` has no effect.
771+
* `syntaxHighlighting` {boolean} If `true`, enables inline syntax
772+
highlighting of REPL input using ANSI color codes. Keywords are displayed
773+
in magenta, strings in green, numbers in yellow, regular expressions in
774+
red, and comments in gray. Requires `useColors` to be `true` and `terminal` to be `true`.
775+
**Default:** `true`.
750776
* `handleError` {Function} This function customizes error handling in the REPL.
751777
It receives the thrown exception as its first argument and must return one
752778
of the following values synchronously:

lib/internal/repl/utils.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,98 @@ function isRecoverableError(e, code) {
143143
}
144144
}
145145

146+
const { stylizeWithColor } = require('internal/util/inspect');
147+
const { Parser } = require('internal/deps/acorn/acorn/dist/acorn');
148+
149+
const tokenizer = FunctionPrototypeBind(Parser.tokenizer, Parser);
150+
151+
/**
152+
* Maps an Acorn token to a util.inspect style name.
153+
* Returns undefined for tokens that should not be highlighted.
154+
* @param {object} token The Acorn token.
155+
* @returns {string|undefined} The inspect style name.
156+
*/
157+
function tokenStyle(token) {
158+
const { label, keyword } = token.type;
159+
160+
if (keyword !== undefined) {
161+
if (label === 'true' || label === 'false') return 'boolean';
162+
if (label === 'null') return 'null';
163+
if (label === 'import' || label === 'export') return 'module';
164+
return 'special';
165+
}
166+
167+
switch (label) {
168+
case 'name':
169+
switch (token.value) {
170+
case 'undefined':
171+
return 'undefined';
172+
case 'NaN':
173+
case 'Infinity':
174+
return 'number';
175+
default:
176+
return undefined;
177+
}
178+
case 'num':
179+
return 'number';
180+
case 'bigint':
181+
return 'bigint';
182+
case 'string':
183+
case 'template':
184+
case '`':
185+
return 'string';
186+
case 'regexp':
187+
return 'regexp';
188+
default:
189+
return undefined;
190+
}
191+
}
192+
193+
/**
194+
* Applies ANSI syntax highlighting to a JavaScript code string.
195+
* Uses the Acorn tokenizer and util.inspect.styles for colors,
196+
* so the color scheme stays in sync with util.inspect output.
197+
* @param {string} code The JavaScript code string to highlight.
198+
* @returns {string} The highlighted code string.
199+
*/
200+
function syntaxHighlight(code) {
201+
if (code.length === 0) return code;
202+
203+
let result = '';
204+
let offset = 0;
205+
206+
function write(start, end, inspectStyle) {
207+
result +=
208+
StringPrototypeSlice(code, offset, start) +
209+
stylizeWithColor(StringPrototypeSlice(code, start, end), inspectStyle);
210+
offset = end;
211+
}
212+
213+
try {
214+
const iterator = tokenizer(code, {
215+
__proto__: null,
216+
allowHashBang: true,
217+
ecmaVersion: 'latest',
218+
onComment(_block, _text, start, end) {
219+
write(start, end, 'undefined');
220+
},
221+
});
222+
223+
for (const token of iterator) {
224+
const inspectStyle = tokenStyle(token);
225+
if (inspectStyle !== undefined) {
226+
write(token.start, token.end, inspectStyle);
227+
}
228+
}
229+
} catch {
230+
// Acorn throws for unfinished strings, templates, and comments.
231+
// Tokens reported before the error are still useful while typing.
232+
}
233+
234+
return result + StringPrototypeSlice(code, offset);
235+
}
236+
237+
146238
function setupPreview(repl, contextSymbol, bufferSymbol, active) {
147239
// Simple terminals can't handle previews.
148240
if (process.env.TERM === 'dumb' || !active) {
@@ -866,6 +958,7 @@ module.exports = {
866958
REPL_MODE_STRICT,
867959
isRecoverableError,
868960
kStandaloneREPL: Symbol('kStandaloneREPL'),
961+
syntaxHighlight,
869962
setupPreview,
870963
setupReverseSearch,
871964
isObjectLiteral,

lib/repl.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const {
127127
REPL_MODE_STRICT,
128128
kStandaloneREPL,
129129
setupPreview,
130+
syntaxHighlight,
130131
setupReverseSearch,
131132
kContextId,
132133
getREPLResourceName,
@@ -239,13 +240,18 @@ class REPLServer extends Interface {
239240
const preview = options.terminal &&
240241
(options.preview !== undefined ? !!options.preview : !eval_);
241242

243+
const syntaxHighlightingEnabled = options.terminal &&
244+
(options.syntaxHighlighting !== undefined ?
245+
!!options.syntaxHighlighting : true);
246+
242247
super({
243248
input: options.input,
244249
output: options.output,
245250
completer: options.completer || completer,
246251
terminal: options.terminal,
247252
historySize: options.historySize,
248253
prompt,
254+
colorize: syntaxHighlightingEnabled ? syntaxHighlight : undefined,
249255
});
250256

251257
ObjectDefineProperty(this, 'inputStream', {

0 commit comments

Comments
 (0)