Skip to content

Commit 3bbdf10

Browse files
repl: add customizable subprompt for multiline input
Add option to customize the REPL subprompt for multiline input.
1 parent 48aa9c7 commit 3bbdf10

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
@@ -415,6 +416,23 @@ class Interface extends InterfaceConstructor {
415416
});
416417
}
417418

419+
/**
420+
* Sets the multiline prompt.
421+
* @param {string} prompt
422+
* @returns {void}
423+
*/
424+
setMultilinePrompt(prompt) {
425+
this[kMultilinePrompt].description = prompt;
426+
}
427+
428+
/**
429+
* Returns the current multiline prompt.
430+
* @returns {string}
431+
*/
432+
getMultilinePrompt() {
433+
return this[kMultilinePrompt].description;
434+
}
435+
418436
[kSetRawMode](mode) {
419437
const wasInRawMode = this.input.isRaw;
420438

@@ -522,7 +540,7 @@ class Interface extends InterfaceConstructor {
522540

523541
// For continuation lines, add the "|" prefix
524542
for (let i = 1; i < lines.length; i++) {
525-
this[kWriteToOutput](`\n${kMultilinePrompt.description}` + lines[i]);
543+
this[kWriteToOutput](`\n${this[kMultilinePrompt].description}` + lines[i]);
526544
}
527545
} else {
528546
// Write the prompt and the current buffer content.
@@ -987,7 +1005,8 @@ class Interface extends InterfaceConstructor {
9871005
const dy = splitEnd.length + 1;
9881006

9891007
// Calculate how many Xs we need to move on the right to get to the end of the line
990-
const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length + kMultilinePrompt.description.length;
1008+
const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length +
1009+
this[kMultilinePrompt].description.length;
9911010
moveCursor(this.output, dxEndOfLineAbove, -dy);
9921011

9931012
// This is the line that was split in the middle
@@ -1008,9 +1027,9 @@ class Interface extends InterfaceConstructor {
10081027
}
10091028

10101029
if (needsRewriteFirstLine) {
1011-
this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`);
1030+
this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${this[kMultilinePrompt].description}`);
10121031
} else {
1013-
this[kWriteToOutput](kMultilinePrompt.description);
1032+
this[kWriteToOutput](this[kMultilinePrompt].description);
10141033
}
10151034

10161035
// Write the rest and restore the cursor to where the user left it
@@ -1022,7 +1041,7 @@ class Interface extends InterfaceConstructor {
10221041
const formattedEndContent = StringPrototypeReplaceAll(
10231042
afterCursor,
10241043
'\n',
1025-
`\n${kMultilinePrompt.description}`,
1044+
`\n${this[kMultilinePrompt].description}`,
10261045
);
10271046

10281047
this[kWriteToOutput](formattedEndContent);
@@ -1083,7 +1102,7 @@ class Interface extends InterfaceConstructor {
10831102
const curr = splitLines[rows];
10841103
const down = direction === 1;
10851104
const adj = splitLines[rows + direction];
1086-
const promptLen = kMultilinePrompt.description.length;
1105+
const promptLen = this[kMultilinePrompt].description.length;
10871106
let amountToMove;
10881107
// Clamp distance to end of current + prompt + next/prev line + newline
10891108
const clamp = down ?
@@ -1174,7 +1193,7 @@ class Interface extends InterfaceConstructor {
11741193
// Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
11751194
rows += MathCeil(offset / col) || 1;
11761195
// Only add prefix offset for continuation lines in user input (not prompts)
1177-
offset = this[kIsMultiline] ? kMultilinePrompt.description.length : 0;
1196+
offset = this[kIsMultiline] ? this[kMultilinePrompt].description.length : 0;
11781197
continue;
11791198
}
11801199
// 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
@@ -1212,7 +1212,8 @@ REPLServer.prototype.resetContext = function() {
12121212
REPLServer.prototype.displayPrompt = function(preserveCursor) {
12131213
let prompt = this._initialPrompt;
12141214
if (this[kBufferedCommandSymbol].length) {
1215-
prompt = kMultilinePrompt.description;
1215+
this[kMultilinePrompt].description = '| ';
1216+
prompt = this[kMultilinePrompt].description;
12161217
}
12171218

12181219
// 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)