diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index bc8f30427616..b7ec191e0db4 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -126,6 +126,9 @@ const kOnLine = Symbol('_onLine'); const kSetLine = Symbol('_setLine'); const kPreviousKey = Symbol('_previousKey'); const kPrompt = Symbol('_prompt'); +const kPromptInvoked = Symbol('_promptInvoked'); +const kEffectivePrompt = Symbol('_effectivePrompt'); +const kDefaultPrompt = '> '; const kPushToKillRing = Symbol('_pushToKillRing'); const kPushToUndoStack = Symbol('_pushToUndoStack'); const kQuestionCallback = Symbol('_questionCallback'); @@ -173,7 +176,7 @@ function InterfaceConstructor(input, output, completer, terminal) { FunctionPrototypeCall(EventEmitter, this); let crlfDelay; - let prompt = '> '; + let prompt = kDefaultPrompt; let signal; let historyOptions; @@ -260,6 +263,7 @@ function InterfaceConstructor(input, output, completer, terminal) { this.completer = completer; this.setPrompt(prompt); + this[kPromptInvoked] = false; this.terminal = !!terminal; @@ -440,6 +444,7 @@ class Interface extends InterfaceConstructor { */ prompt(preserveCursor) { if (this.paused) this.resume(); + this[kPromptInvoked] = true; if (this.terminal && process.env.TERM !== 'dumb') { if (!preserveCursor) this.cursor = 0; this[kRefreshLine](); @@ -448,6 +453,15 @@ class Interface extends InterfaceConstructor { } } + // Issue #12606: default prompt is not painted until `prompt()`, but + // non-default prompts participate in layout/cursor math as before. + [kEffectivePrompt]() { + if (this[kPromptInvoked]) { + return this[kPrompt]; + } + return this[kPrompt] === kDefaultPrompt ? '' : this[kPrompt]; + } + [kQuestion](query, cb) { if (this.closed) { throw new ERR_USE_AFTER_CLOSE('readline'); @@ -504,7 +518,8 @@ class Interface extends InterfaceConstructor { [kRefreshLine]() { // line length - const line = this[kPrompt] + this.line; + const promptPrefix = this[kEffectivePrompt](); + const line = promptPrefix + this.line; const dispPos = this[kGetDisplayPos](line); const lineCols = dispPos.cols; const lineRows = dispPos.rows; @@ -526,7 +541,7 @@ class Interface extends InterfaceConstructor { if (this[kIsMultiline]) { const lines = StringPrototypeSplit(this.line, '\n'); // Write first line with normal prompt - this[kWriteToOutput](this[kPrompt] + + this[kWriteToOutput](promptPrefix + (this[kColorize]?.(lines[0]) ?? lines[0])); // For continuation lines, add the "|" prefix @@ -536,7 +551,7 @@ class Interface extends InterfaceConstructor { } } else { // Write the prompt and the current buffer content. - this[kWriteToOutput](this[kPrompt] + + this[kWriteToOutput](promptPrefix + (this[kColorize]?.(this.line) ?? this.line)); } @@ -772,6 +787,9 @@ class Interface extends InterfaceConstructor { } this[kBeforeEdit](this.line, this.cursor); + // Tab completion redraws the whole line; treat it like `prompt()` for the + // purpose of painting the default prompt (see #12606). + this[kPromptInvoked] = true; // Apply/show completions. const completionsWidth = ArrayPrototypeMap(completions, (e) => @@ -1045,8 +1063,10 @@ class Interface extends InterfaceConstructor { } } - if (needsRewriteFirstLine) { - this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`); + if (needsRewriteFirstLine) { + this[kWriteToOutput]( + `${this[kEffectivePrompt]()}${beforeCursor}\n${kMultilinePrompt.description}`, + ); } else { this[kWriteToOutput](kMultilinePrompt.description); } @@ -1247,7 +1267,8 @@ class Interface extends InterfaceConstructor { * }} */ getCursorPos() { - const strBeforeCursor = this[kPrompt] + StringPrototypeSlice(this.line, 0, this.cursor); + const strBeforeCursor = this[kEffectivePrompt]() + + StringPrototypeSlice(this.line, 0, this.cursor); return this[kGetDisplayPos](strBeforeCursor); }