From a8edad69ed3866a4613aaec5f8a9efdafafec683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20He=C3=9F?= Date: Thu, 10 Sep 2026 14:38:25 +0200 Subject: [PATCH] Lite Terminal: don't repaint a pasted block taller than the viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coloring a submitted paste repainted the input in place: save the cursor, move up the number of rows computed for the input, erase, redraw, restore. That upward move is unfulfillable once the input is taller than the window — `\x1b[nA` stops at the top row — so the redraw started as many rows too low as the input exceeded the viewport, leaving the top of the original block on screen and the command's output inside the second copy. Measured with a 31-line paste: in a 49-row window the handler moves up 30 rows and the result is correct; in a 20-row window it computes the same 30 and lands 11 rows off, which is exactly the overflow. Input that arrives with its own carriage return is submitted straight away, so its echo is never corrected by a later keystroke. Hold that echo back and let the `color` handler write the colored text in its place. A plain write at the position the raw text would have occupied cannot be misplaced, so no arithmetic is involved and the height of the block stops mattering. If no coloring arrives inside the existing 100 ms window, the raw text is written instead: uncolored as before, but never invisible. Typed input keeps the existing repaint, which is correct for it — every keystroke colors a block that is already on screen. Co-Authored-By: Claude Opus 5 --- src/commands/webSocketTerminal.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/commands/webSocketTerminal.ts b/src/commands/webSocketTerminal.ts index c5b138fe..dad4fb85 100644 --- a/src/commands/webSocketTerminal.ts +++ b/src/commands/webSocketTerminal.ts @@ -88,6 +88,9 @@ class WebSocketTerminal implements vscode.Pseudoterminal { /** The number of columns in the terminal */ private _cols: number; + /** The echo held back for input that submits itself, until its coloring arrives */ + private _echoHeld?: string; + /** The `RegExp` used to strip ANSI color escape codes from a string */ // eslint-disable-next-line no-control-regex private _colorsRegex = /\x1b[^m]*?m/g; @@ -297,6 +300,13 @@ class WebSocketTerminal implements vscode.Pseudoterminal { case "color": { // Outdated: the input is no longer on screen if (this._state != "prompt") break; + if (this._echoHeld != undefined) { + // This input was never echoed, so write the colored text where the raw text would + // have gone. A plain write cannot be misplaced, however tall the input is. + this._echoHeld = undefined; + this._hideCursorWrite(message.text.replace(/\r\n/g, `\r\n${this.multiLinePrompt}`)); + break; + } // Replace the input with the syntax colored text, keeping the cursor at the same spot let cursorLine = Math.ceil((this._cursorCol + 1) / this._cols) - 1; if (message.text.includes("\r\n")) { @@ -664,14 +674,27 @@ class WebSocketTerminal implements vscode.Pseudoterminal { lines.unshift(firstLine); char = lines.join("\r\n"); } - // Save the cursor position, write the text, restore the cursor position, then move the cursor manually - this._hideCursorWrite(`\x1b7${eraseAfterCursor}${char}\x1b8${rowStr}${colStr}`); + // Input that arrives with its own carriage return is submitted straight away, so its echo + // is never corrected by a later keystroke. Hold it back and let the `color` handler write + // the colored text in its place: repainting over text already on screen has to move the + // cursor up, and that move is unfulfillable once the input is taller than the viewport. + if (submit && this._state == "prompt" && this._input != "") { + this._echoHeld = char; + } else { + // Save the cursor position, write the text, restore the cursor position, then move the cursor manually + this._hideCursorWrite(`\x1b7${eraseAfterCursor}${char}\x1b8${rowStr}${colStr}`); + } if (this._input != "" && this._state == "prompt") { this._socket.send(JSON.stringify({ type: "color", input: this._input })); } if (submit) { // Let the coloring arrive before submitting moves the input off its line await new Promise((resolve) => setTimeout(resolve, 100)); + if (this._echoHeld != undefined) { + // No coloring arrived in time, so write the raw text: uncolored, but never invisible + this._hideCursorWrite(this._echoHeld); + this._echoHeld = undefined; + } if (this._state == "prompt") { // Reset historyIdx this._historyIdx = -1;