diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 08f7aaa9e3e7e8..0e128c46726e36 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'); @@ -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,6 +237,9 @@ function InterfaceConstructor(input, output, completer, terminal) { this[kUndoStack] = []; this[kRedoStack] = []; this[kPreviousCursorCols] = -1; + 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 @@ -518,7 +521,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. @@ -999,7 +1002,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 @@ -1020,9 +1024,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 @@ -1034,7 +1038,7 @@ class Interface extends InterfaceConstructor { const formattedEndContent = StringPrototypeReplaceAll( afterCursor, '\n', - `\n${kMultilinePrompt.description}`, + `\n${this[kMultilinePrompt].description}`, ); this[kWriteToOutput](formattedEndContent); @@ -1095,7 +1099,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 ? @@ -1186,7 +1190,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/repl.js b/lib/repl.js index 5ad9e4fbb1506f..f878e72067ebab 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -756,6 +756,19 @@ class REPLServer extends Interface { FunctionPrototypeCall(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(); this.commands = { __proto__: null }; @@ -1143,7 +1156,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..9b8f8ff54c5841 --- /dev/null +++ b/test/parallel/test-repl-multiline-prompt.js @@ -0,0 +1,51 @@ +'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, + multilinePrompt: 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 });