Skip to content

Commit dc03ff2

Browse files
repl: add customizable subprompt for multiline input
Add option to customize the REPL subprompt for multiline input.
1 parent 4612c79 commit dc03ff2

File tree

4 files changed

+83
-9
lines changed

4 files changed

+83
-9
lines changed

lib/internal/readline/interface.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const ESCAPE_CODE_TIMEOUT = 500;
9797
// Max length of the kill ring
9898
const kMaxLengthOfKillRing = 32;
9999

100-
const kMultilinePrompt = Symbol('| ');
100+
const kMultilinePrompt = Symbol('multilinePrompt');
101101

102102
const kAddHistory = Symbol('_addHistory');
103103
const kBeforeEdit = Symbol('_beforeEdit');
@@ -237,6 +237,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
237237
this[kUndoStack] = [];
238238
this[kRedoStack] = [];
239239
this[kPreviousCursorCols] = -1;
240+
this[kMultilinePrompt] ||= { description: '| ' };
240241

241242
// The kill ring is a global list of blocks of text that were previously
242243
// killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest
@@ -411,6 +412,23 @@ class Interface extends InterfaceConstructor {
411412
});
412413
}
413414

415+
/**
416+
* Sets the multiline prompt.
417+
* @param {string} prompt
418+
* @returns {void}
419+
*/
420+
setMultilinePrompt(prompt) {
421+
this[kMultilinePrompt].description = prompt;
422+
}
423+
424+
/**
425+
* Returns the current multiline prompt.
426+
* @returns {string}
427+
*/
428+
getMultilinePrompt() {
429+
return this[kMultilinePrompt].description;
430+
}
431+
414432
[kSetRawMode](mode) {
415433
const wasInRawMode = this.input.isRaw;
416434

@@ -518,7 +536,7 @@ class Interface extends InterfaceConstructor {
518536

519537
// For continuation lines, add the "|" prefix
520538
for (let i = 1; i < lines.length; i++) {
521-
this[kWriteToOutput](`\n${kMultilinePrompt.description}` + lines[i]);
539+
this[kWriteToOutput](`\n${this[kMultilinePrompt].description}` + lines[i]);
522540
}
523541
} else {
524542
// Write the prompt and the current buffer content.
@@ -989,7 +1007,8 @@ class Interface extends InterfaceConstructor {
9891007
const dy = splitEnd.length + 1;
9901008

9911009
// Calculate how many Xs we need to move on the right to get to the end of the line
992-
const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length + kMultilinePrompt.description.length;
1010+
const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length +
1011+
this[kMultilinePrompt].description.length;
9931012
moveCursor(this.output, dxEndOfLineAbove, -dy);
9941013

9951014
// This is the line that was split in the middle
@@ -1010,9 +1029,9 @@ class Interface extends InterfaceConstructor {
10101029
}
10111030

10121031
if (needsRewriteFirstLine) {
1013-
this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`);
1032+
this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${this[kMultilinePrompt].description}`);
10141033
} else {
1015-
this[kWriteToOutput](kMultilinePrompt.description);
1034+
this[kWriteToOutput](this[kMultilinePrompt].description);
10161035
}
10171036

10181037
// Write the rest and restore the cursor to where the user left it
@@ -1024,7 +1043,7 @@ class Interface extends InterfaceConstructor {
10241043
const formattedEndContent = StringPrototypeReplaceAll(
10251044
afterCursor,
10261045
'\n',
1027-
`\n${kMultilinePrompt.description}`,
1046+
`\n${this[kMultilinePrompt].description}`,
10281047
);
10291048

10301049
this[kWriteToOutput](formattedEndContent);
@@ -1085,7 +1104,7 @@ class Interface extends InterfaceConstructor {
10851104
const curr = splitLines[rows];
10861105
const down = direction === 1;
10871106
const adj = splitLines[rows + direction];
1088-
const promptLen = kMultilinePrompt.description.length;
1107+
const promptLen = this[kMultilinePrompt].description.length;
10891108
let amountToMove;
10901109
// Clamp distance to end of current + prompt + next/prev line + newline
10911110
const clamp = down ?
@@ -1176,7 +1195,7 @@ class Interface extends InterfaceConstructor {
11761195
// Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
11771196
rows += MathCeil(offset / col) || 1;
11781197
// Only add prefix offset for continuation lines in user input (not prompts)
1179-
offset = this[kIsMultiline] ? kMultilinePrompt.description.length : 0;
1198+
offset = this[kIsMultiline] ? this[kMultilinePrompt].description.length : 0;
11801199
continue;
11811200
}
11821201
// Tabs must be aligned by an offset of the tab size.

lib/internal/repl.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function createRepl(env, opts, cb) {
2222
ignoreUndefined: false,
2323
useGlobal: true,
2424
breakEvalOnSigint: true,
25+
multilinePrompt: opts?.multilinePrompt ?? '| ',
2526
...opts,
2627
};
2728

lib/repl.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1192,7 +1192,8 @@ class REPLServer extends Interface {
11921192
displayPrompt(preserveCursor) {
11931193
let prompt = this._initialPrompt;
11941194
if (this[kBufferedCommandSymbol].length) {
1195-
prompt = kMultilinePrompt.description;
1195+
this[kMultilinePrompt].description = '| ';
1196+
prompt = this[kMultilinePrompt].description;
11961197
}
11971198

11981199
// Do not overwrite `_initialPrompt` here
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
const common = require('../common');
3+
const ArrayStream = require('../common/arraystream');
4+
const assert = require('assert');
5+
const repl = require('repl');
6+
7+
const input = [
8+
'const foo = {', // start object
9+
'};', // end object
10+
'foo', // evaluate variable
11+
];
12+
13+
function runPromptTest(promptStr, { useColors }) {
14+
const inputStream = new ArrayStream();
15+
const outputStream = new ArrayStream();
16+
let output = '';
17+
18+
outputStream.write = (data) => { output += data.replace('\r', ''); };
19+
20+
const r = repl.start({
21+
prompt: '',
22+
input: inputStream,
23+
output: outputStream,
24+
terminal: true,
25+
useColors
26+
});
27+
28+
// Set the custom multiline prompt
29+
r.setMultilinePrompt(promptStr);
30+
31+
r.on('exit', common.mustCall(() => {
32+
const lines = output.split('\n');
33+
34+
// Validate REPL output
35+
assert.ok(lines[0].endsWith(input[0])); // first line
36+
assert.ok(lines[1].includes(promptStr)); // continuation line
37+
assert.ok(lines[1].endsWith(input[1])); // second line content
38+
assert.ok(lines[2].includes('undefined')); // first eval result
39+
assert.ok(lines[3].endsWith(input[2])); // final variable
40+
assert.ok(lines[4].includes('{}')); // printed object
41+
}));
42+
43+
inputStream.run(input);
44+
r.close();
45+
}
46+
47+
// Test with custom `... ` prompt
48+
runPromptTest('... ', { useColors: true });
49+
runPromptTest('... ', { useColors: false });
50+
51+
// Test with default `| ` prompt
52+
runPromptTest('| ', { useColors: true });
53+
runPromptTest('| ', { useColors: false });

0 commit comments

Comments
 (0)