From dc03ff208fe0ad7baf50ea80edc2c727c0b15a6f Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Thu, 21 Aug 2025 14:34:48 +0530 Subject: [PATCH 1/4] 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 46674883381392..fee8c69769a5c6 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 @@ -411,6 +412,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; @@ -518,7 +536,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. @@ -989,7 +1007,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 @@ -1010,9 +1029,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 @@ -1024,7 +1043,7 @@ class Interface extends InterfaceConstructor { const formattedEndContent = StringPrototypeReplaceAll( afterCursor, '\n', - `\n${kMultilinePrompt.description}`, + `\n${this[kMultilinePrompt].description}`, ); this[kWriteToOutput](formattedEndContent); @@ -1085,7 +1104,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 ? @@ -1176,7 +1195,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 fbe85c7b89c0a6..8e0f110c1d6dd8 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1192,7 +1192,8 @@ class REPLServer extends Interface { displayPrompt(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 }); From 494e89a9aa4b0ce4240b0827b4c2b373ac61cff5 Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Fri, 19 Sep 2025 21:08:07 +0530 Subject: [PATCH 2/4] repl: support customizable multiline subprompt Add a option to the REPL to allow custom prompts for multiline input, replacing the default --- lib/internal/readline/interface.js | 23 ++++----------------- lib/internal/repl.js | 1 - lib/repl.js | 22 +++++++++++++++----- test/parallel/test-repl-multiline-prompt.js | 6 ++---- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index fee8c69769a5c6..1d761da3025c96 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -172,6 +172,7 @@ function InterfaceConstructor(input, output, completer, terminal) { let crlfDelay; let prompt = '> '; let signal; + const userMultilinePrompt = input?.multilinePrompt ?? '| '; if (input?.input) { // An options object was given @@ -228,7 +229,6 @@ function InterfaceConstructor(input, output, completer, terminal) { } const self = this; - this.line = ''; this[kIsMultiline] = false; this[kSubstringSearch] = null; @@ -237,7 +237,9 @@ function InterfaceConstructor(input, output, completer, terminal) { this[kUndoStack] = []; this[kRedoStack] = []; this[kPreviousCursorCols] = -1; - this[kMultilinePrompt] ||= { description: '| ' }; + this[kMultilinePrompt] = { + description: userMultilinePrompt, + }; // The kill ring is a global list of blocks of text that were previously // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest @@ -412,23 +414,6 @@ 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; diff --git a/lib/internal/repl.js b/lib/internal/repl.js index 06705e823dc54b..2552aabf173e0d 100644 --- a/lib/internal/repl.js +++ b/lib/internal/repl.js @@ -22,7 +22,6 @@ 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 8e0f110c1d6dd8..0955a08fe3c4e8 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -800,10 +800,22 @@ class REPLServer extends Interface { self.clearBufferedCommand(); - function completer(text, cb) { - ReflectApply(complete, self, - [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); - } + function completer(text, cb) { + ReflectApply(complete, self, + [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); + } + + // All the parameters in the object are defining the "input" param of the + // InterfaceConstructor. + ReflectApply(Interface, this, [{ + input: options.input, + output: options.output, + completer: options.completer || completer, + terminal: options.terminal, + historySize: options.historySize, + prompt, + multilinePrompt: options.multilinePrompt, + }]); self.resetContext(); @@ -1192,7 +1204,7 @@ class REPLServer extends Interface { displayPrompt(preserveCursor) { let prompt = this._initialPrompt; if (this[kBufferedCommandSymbol].length) { - this[kMultilinePrompt].description = '| '; + // this[kMultilinePrompt].description = '| '; prompt = this[kMultilinePrompt].description; } diff --git a/test/parallel/test-repl-multiline-prompt.js b/test/parallel/test-repl-multiline-prompt.js index e2a436bd7432d1..9b8f8ff54c5841 100644 --- a/test/parallel/test-repl-multiline-prompt.js +++ b/test/parallel/test-repl-multiline-prompt.js @@ -22,12 +22,10 @@ function runPromptTest(promptStr, { useColors }) { input: inputStream, output: outputStream, terminal: true, - useColors + useColors, + multilinePrompt: promptStr, }); - // Set the custom multiline prompt - r.setMultilinePrompt(promptStr); - r.on('exit', common.mustCall(() => { const lines = output.split('\n'); From 02f1d5e75d8520bfcde04890f2de41feb5157fce Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Fri, 19 Sep 2025 21:41:43 +0530 Subject: [PATCH 3/4] repl: support customizable multiline subprompt lint fixes for repl file. --- lib/repl.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index 0955a08fe3c4e8..5495f0c48920aa 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -800,22 +800,26 @@ class REPLServer extends Interface { self.clearBufferedCommand(); - function completer(text, cb) { - ReflectApply(complete, self, - [text, self.editorMode ? self.completeOnEditorMode(cb) : cb]); - } + function completer(text, cb) { + ReflectApply( + complete, + self, + [text, self.editorMode ? self.completeOnEditorMode(cb) : cb], + ); + } - // All the parameters in the object are defining the "input" param of the - // InterfaceConstructor. - ReflectApply(Interface, this, [{ - input: options.input, - output: options.output, - completer: options.completer || completer, - terminal: options.terminal, - historySize: options.historySize, - prompt, - multilinePrompt: options.multilinePrompt, - }]); + + // All the parameters in the object are defining the "input" param of the + // InterfaceConstructor. + ReflectApply(Interface, this, [{ + input: options.input, + output: options.output, + completer: options.completer || completer, + terminal: options.terminal, + historySize: options.historySize, + prompt, + multilinePrompt: options.multilinePrompt, + }]); self.resetContext(); From de9083b885e818d9821a4122f09c229fe0283512 Mon Sep 17 00:00:00 2001 From: krishnadas Date: Mon, 1 Dec 2025 01:45:02 +0530 Subject: [PATCH 4/4] repl: fix ESLint errors in repl module --- lib/repl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index d81bbc0ccbe8b4..f878e72067ebab 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -759,7 +759,7 @@ class REPLServer extends Interface { // All the parameters in the object are defining the "input" param of the // InterfaceConstructor. - ReflectApply(Interface, this, [{ + ReflectApply(Interface, this, { input: options.input, output: options.output, completer: options.completer || completer, @@ -767,7 +767,7 @@ class REPLServer extends Interface { historySize: options.historySize, prompt, multilinePrompt: options.multilinePrompt, - }]); + }); self.resetContext();