Skip to content

Commit 405267e

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 405267e

4 files changed

Lines changed: 418 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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const {
4545
const {
4646
getStringWidth,
4747
inspect,
48+
stylizeWithColor,
4849
} = require('internal/util/inspect');
4950

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

147+
/**
148+
* Maps an Acorn token to a util.inspect style name.
149+
* Returns undefined for tokens that should not be highlighted.
150+
* @param {object} token The Acorn token.
151+
* @returns {string|undefined} The inspect style name.
152+
*/
153+
function tokenStyle(token) {
154+
const { label, keyword } = token.type;
155+
156+
if (keyword !== undefined) {
157+
if (label === 'true' || label === 'false') return 'boolean';
158+
if (label === 'null') return 'null';
159+
if (label === 'import' || label === 'export') return 'module';
160+
return 'special';
161+
}
162+
163+
switch (label) {
164+
case 'name':
165+
switch (token.value) {
166+
case 'undefined':
167+
return 'undefined';
168+
case 'NaN':
169+
case 'Infinity':
170+
return 'number';
171+
default:
172+
return undefined;
173+
}
174+
case 'num':
175+
return 'number';
176+
case 'bigint':
177+
return 'bigint';
178+
case 'string':
179+
case 'template':
180+
case '`':
181+
return 'string';
182+
case 'regexp':
183+
return 'regexp';
184+
default:
185+
return undefined;
186+
}
187+
}
188+
189+
/**
190+
* Applies ANSI syntax highlighting to a JavaScript code string.
191+
* Uses the Acorn tokenizer and util.inspect.styles for colors,
192+
* so the color scheme stays in sync with util.inspect output.
193+
* @param {string} code The JavaScript code string to highlight.
194+
* @returns {string} The highlighted code string.
195+
*/
196+
function syntaxHighlight(code) {
197+
if (code.length === 0) return code;
198+
199+
const { Parser } =
200+
require('internal/deps/acorn/acorn/dist/acorn');
201+
const tokenize = FunctionPrototypeBind(Parser.tokenizer, Parser);
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 = tokenize(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+
146237
function setupPreview(repl, contextSymbol, bufferSymbol, active) {
147238
// Simple terminals can't handle previews.
148239
if (process.env.TERM === 'dumb' || !active) {
@@ -866,6 +957,7 @@ module.exports = {
866957
REPL_MODE_STRICT,
867958
isRecoverableError,
868959
kStandaloneREPL: Symbol('kStandaloneREPL'),
960+
syntaxHighlight,
869961
setupPreview,
870962
setupReverseSearch,
871963
isObjectLiteral,

lib/repl.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const {
128128
kStandaloneREPL,
129129
setupPreview,
130130
setupReverseSearch,
131+
syntaxHighlight,
131132
kContextId,
132133
getREPLResourceName,
133134
getGlobalBuiltins,
@@ -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)