From 3bbdf10daf1b5c643807459df01c6d6afa403455 Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Thu, 21 Aug 2025 14:34:48 +0530 Subject: [PATCH] repl: add customizable subprompt for multiline input Add option to customize the REPL subprompt for multiline input. --- lib/internal/readline/interface.js | 35 ++++++++++---- lib/internal/repl.js | 1 + lib/repl.js | 3 +- test/parallel/test-repl-multiline-prompt.js | 53 +++++++++++++++++++++ 4 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 test/parallel/test-repl-multiline-prompt.js diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 5ebfa44ecba068..5f604a982c983a 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -97,7 +97,7 @@ const ESCAPE_CODE_TIMEOUT = 500; // Max length of the kill ring const kMaxLengthOfKillRing = 32; -const kMultilinePrompt = Symbol('| '); +const kMultilinePrompt = Symbol('multilinePrompt'); const kAddHistory = Symbol('_addHistory'); const kBeforeEdit = Symbol('_beforeEdit'); @@ -237,6 +237,7 @@ function InterfaceConstructor(input, output, completer, terminal) { this[kUndoStack] = []; this[kRedoStack] = []; this[kPreviousCursorCols] = -1; + this[kMultilinePrompt] ||= { description: '| ' }; // The kill ring is a global list of blocks of text that were previously // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest @@ -415,6 +416,23 @@ class Interface extends InterfaceConstructor { }); } + /** + * Sets the multiline prompt. + * @param {string} prompt + * @returns {void} + */ + setMultilinePrompt(prompt) { + this[kMultilinePrompt].description = prompt; + } + + /** + * Returns the current multiline prompt. + * @returns {string} + */ + getMultilinePrompt() { + return this[kMultilinePrompt].description; + } + [kSetRawMode](mode) { const wasInRawMode = this.input.isRaw; @@ -522,7 +540,7 @@ class Interface extends InterfaceConstructor { // For continuation lines, add the "|" prefix for (let i = 1; i < lines.length; i++) { - this[kWriteToOutput](`\n${kMultilinePrompt.description}` + lines[i]); + this[kWriteToOutput](`\n${this[kMultilinePrompt].description}` + lines[i]); } } else { // Write the prompt and the current buffer content. @@ -987,7 +1005,8 @@ class Interface extends InterfaceConstructor { const dy = splitEnd.length + 1; // Calculate how many Xs we need to move on the right to get to the end of the line - const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length + kMultilinePrompt.description.length; + const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length + + this[kMultilinePrompt].description.length; moveCursor(this.output, dxEndOfLineAbove, -dy); // This is the line that was split in the middle @@ -1008,9 +1027,9 @@ class Interface extends InterfaceConstructor { } if (needsRewriteFirstLine) { - this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`); + this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${this[kMultilinePrompt].description}`); } else { - this[kWriteToOutput](kMultilinePrompt.description); + this[kWriteToOutput](this[kMultilinePrompt].description); } // Write the rest and restore the cursor to where the user left it @@ -1022,7 +1041,7 @@ class Interface extends InterfaceConstructor { const formattedEndContent = StringPrototypeReplaceAll( afterCursor, '\n', - `\n${kMultilinePrompt.description}`, + `\n${this[kMultilinePrompt].description}`, ); this[kWriteToOutput](formattedEndContent); @@ -1083,7 +1102,7 @@ class Interface extends InterfaceConstructor { const curr = splitLines[rows]; const down = direction === 1; const adj = splitLines[rows + direction]; - const promptLen = kMultilinePrompt.description.length; + const promptLen = this[kMultilinePrompt].description.length; let amountToMove; // Clamp distance to end of current + prompt + next/prev line + newline const clamp = down ? @@ -1174,7 +1193,7 @@ class Interface extends InterfaceConstructor { // Rows must be incremented by 1 even if offset = 0 or col = +Infinity. rows += MathCeil(offset / col) || 1; // Only add prefix offset for continuation lines in user input (not prompts) - offset = this[kIsMultiline] ? kMultilinePrompt.description.length : 0; + offset = this[kIsMultiline] ? this[kMultilinePrompt].description.length : 0; continue; } // Tabs must be aligned by an offset of the tab size. diff --git a/lib/internal/repl.js b/lib/internal/repl.js index 2552aabf173e0d..06705e823dc54b 100644 --- a/lib/internal/repl.js +++ b/lib/internal/repl.js @@ -22,6 +22,7 @@ function createRepl(env, opts, cb) { ignoreUndefined: false, useGlobal: true, breakEvalOnSigint: true, + multilinePrompt: opts?.multilinePrompt ?? '| ', ...opts, }; diff --git a/lib/repl.js b/lib/repl.js index 443971df63b0e8..4c3c95dff7be39 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1212,7 +1212,8 @@ REPLServer.prototype.resetContext = function() { REPLServer.prototype.displayPrompt = function(preserveCursor) { let prompt = this._initialPrompt; if (this[kBufferedCommandSymbol].length) { - prompt = kMultilinePrompt.description; + this[kMultilinePrompt].description = '| '; + prompt = this[kMultilinePrompt].description; } // Do not overwrite `_initialPrompt` here diff --git a/test/parallel/test-repl-multiline-prompt.js b/test/parallel/test-repl-multiline-prompt.js new file mode 100644 index 00000000000000..e2a436bd7432d1 --- /dev/null +++ b/test/parallel/test-repl-multiline-prompt.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../common'); +const ArrayStream = require('../common/arraystream'); +const assert = require('assert'); +const repl = require('repl'); + +const input = [ + 'const foo = {', // start object + '};', // end object + 'foo', // evaluate variable +]; + +function runPromptTest(promptStr, { useColors }) { + const inputStream = new ArrayStream(); + const outputStream = new ArrayStream(); + let output = ''; + + outputStream.write = (data) => { output += data.replace('\r', ''); }; + + const r = repl.start({ + prompt: '', + input: inputStream, + output: outputStream, + terminal: true, + useColors + }); + + // Set the custom multiline prompt + r.setMultilinePrompt(promptStr); + + r.on('exit', common.mustCall(() => { + const lines = output.split('\n'); + + // Validate REPL output + assert.ok(lines[0].endsWith(input[0])); // first line + assert.ok(lines[1].includes(promptStr)); // continuation line + assert.ok(lines[1].endsWith(input[1])); // second line content + assert.ok(lines[2].includes('undefined')); // first eval result + assert.ok(lines[3].endsWith(input[2])); // final variable + assert.ok(lines[4].includes('{}')); // printed object + })); + + inputStream.run(input); + r.close(); +} + +// Test with custom `... ` prompt +runPromptTest('... ', { useColors: true }); +runPromptTest('... ', { useColors: false }); + +// Test with default `| ` prompt +runPromptTest('| ', { useColors: true }); +runPromptTest('| ', { useColors: false });