From 6360024fc54e78dfab492ceb28d7b55e99512ab0 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Mon, 15 Sep 2025 15:28:39 -0400 Subject: [PATCH] Lite Terminal shell integration improvements --- package-lock.json | 10 ++--- package.json | 6 +-- src/commands/webSocketTerminal.ts | 68 +++++++++++++++++++------------ 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ea37b8f..c78a6710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "@types/mocha": "^7.0.2", "@types/node": "20.17.6", "@types/semver": "7.5.4", - "@types/vscode": "1.93.0", + "@types/vscode": "1.104.0", "@types/ws": "8.18.0", "@types/xmldom": "^0.1.34", "@typescript-eslint/eslint-plugin": "^8.15.0", @@ -57,7 +57,7 @@ "webpack-cli": "^6.0.1" }, "engines": { - "vscode": "^1.93.0" + "vscode": "^1.104.0" } }, "node_modules/@discoveryjs/json-ext": { @@ -687,9 +687,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", - "integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==", + "version": "1.104.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.104.0.tgz", + "integrity": "sha512-0KwoU2rZ2ecsTGFxo4K1+f+AErRsYW0fsp6A0zufzGuhyczc2IoKqYqcwXidKXmy2u8YB2GsYsOtiI9Izx3Tig==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 92f69b74..506bd6c5 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ } ], "engines": { - "vscode": "^1.93.0" + "vscode": "^1.104.0" }, "enabledApiProposals": [ "fileSearchProvider", @@ -1805,7 +1805,7 @@ "test": "node ./out/test/runTest.js", "lint": "eslint src/**", "lint-fix": "eslint --fix src/**", - "download-api": "dts dev 1.93.0", + "download-api": "dts dev 1.104.0", "postinstall": "npm run download-api" }, "devDependencies": { @@ -1816,7 +1816,7 @@ "@types/mocha": "^7.0.2", "@types/node": "20.17.6", "@types/semver": "7.5.4", - "@types/vscode": "1.93.0", + "@types/vscode": "1.104.0", "@types/ws": "8.18.0", "@types/xmldom": "^0.1.34", "@typescript-eslint/eslint-plugin": "^8.15.0", diff --git a/src/commands/webSocketTerminal.ts b/src/commands/webSocketTerminal.ts index 250437cc..53e8d99f 100644 --- a/src/commands/webSocketTerminal.ts +++ b/src/commands/webSocketTerminal.ts @@ -56,7 +56,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { /** The number of characters on the line that the user can't delete */ private _margin = 0; - /** The text writted by the user since the last prompt/read */ + /** The text written by the user since the last prompt/read */ private _input = ""; /** The position of the cursor within the line */ @@ -98,6 +98,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { constructor( private readonly _targetUri: vscode.Uri, + private readonly _nonce: string, private readonly _nsOverride?: string ) {} @@ -224,7 +225,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { this._hideCursorWrite("\x1b]633;P;HasRichCommandDetection=True\x07"); // Print the opening message this._hideCursorWrite( - `\x1b[32mConnected to \x1b[0m\x1b[4m${api.config.host}:${api.config.port}${api.config.pathPrefix}\x1b[0m\x1b[32m as \x1b[0m\x1b[3m${api.config.username}\x1b[0m\r\n\r\n` + `\x1b[32mConnected to \x1b[0m\x1b[4m${api.config.host}:${api.config.port}${api.config.pathPrefix}\x1b[0m\x1b[32m as \x1b[0m\x1b[3m${api.config.username}\x1b[0m\r\n` ); // Add event handlers to the socket this._socket @@ -273,9 +274,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { if (message.type == "prompt") { // Write the prompt to the terminal this._hideCursorWrite( - `\x1b]633;D${this._promptExitCode}\x07${this._margin ? "\r\n" : ""}\x1b]633;A\x07${ - message.text - }\x1b]633;B\x07` + `\x1b]633;D${this._promptExitCode}\x07\r\n\x1b]633;A\x07${message.text}\x1b]633;B\x07` ); this._margin = this._cursorCol = message.text.replace(this._colorsRegex, "").length; this._prompt = message.text; @@ -366,13 +365,14 @@ class WebSocketTerminal implements vscode.Pseudoterminal { // Send the input to the server for processing this._socket.send(JSON.stringify({ type: this._state, input: this._input })); if (this._state == "prompt") { - this._hideCursorWrite(`\x1b]633;E;${this._inputEscaped()}\x07\x1b]633;C\x07\r\n`); + this._hideCursorWrite(`\x1b]633;E;${this._inputEscaped()};${this._nonce}\x07\r\n\x1b]633;C\x07`); if (this._input == "") { this._promptExitCode = ""; } } this._input = ""; this._state = "eval"; + this._margin = this._cursorCol = 0; return; } case keys.ctrlH: @@ -561,7 +561,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { if (this._cursorCol == this._margin + inputArr[inputArr.length - 1].length) { // Move the cursor to the beginning of the input this._moveCursor(this._margin - this._cursorCol); - // Erase everyhting to the right of the cursor + // Erase everything to the right of the cursor this._hideCursorWrite("\x1b[0J"); inputArr[inputArr.length - 1] = ""; this._input = inputArr.join("\r\n"); @@ -588,10 +588,16 @@ class WebSocketTerminal implements vscode.Pseudoterminal { // Submit the input after processing // This should only happen due to VS Code's shell integration submit = true; - char = char.slice(0, -1); - } - // Replace all single \r with \r\n (prompt) or space (read) - char = char.replace(/\r/g, this._state == "prompt" ? "\r\n" : " "); + // Need to remove any multi-line prompts that are in the command lines + // Workaround for https://github.com/microsoft/vscode/issues/258457 + char = char + .slice(0, -1) + .split("\r") + .map((l) => (l.startsWith(this._multiLinePrompt) ? l.slice(this._multiLinePrompt.length) : l)) + .join("\r"); + } + // Replace all single \r with \r\n + char = char.replace(/\r(?!\n)/g, "\r\n"); const inputArr = this._input.split("\r\n"); let eraseAfterCursor = "", trailingText = ""; @@ -613,8 +619,10 @@ class WebSocketTerminal implements vscode.Pseudoterminal { const originalCol = this._cursorCol; let newRow: number; if (char.includes("\r\n")) { - char = char.replace(/\r\n/g, `\r\n${this._multiLinePrompt}`); - this._margin = this._multiLinePrompt.length; + if (this._state == "prompt") { + char = char.replaceAll("\r\n", `\r\n${this._multiLinePrompt}`); + this._margin = this._multiLinePrompt.length; + } const charLines = char.split("\r\n"); newRow = charLines.reduce( @@ -632,21 +640,24 @@ class WebSocketTerminal implements vscode.Pseudoterminal { const colStr = colDelta ? (colDelta > 0 ? `\x1b[${colDelta}C` : `\x1b[${Math.abs(colDelta)}D`) : ""; char += trailingText; const spaceOnCurrentLine = this._cols - (originalCol % this._cols); - if (this._state == "read" && char.length >= spaceOnCurrentLine) { + if (this._state == "read" && (char.includes("\r\n") || char.length >= spaceOnCurrentLine)) { // There's no auto-line wrapping when in read mode, so we must move the cursor manually + const charLines = char.split("\r\n"); // Extract all the characters that fit on the cursor's line - const firstLine = char.slice(0, spaceOnCurrentLine); - const otherLines = char.slice(spaceOnCurrentLine); - const lines: string[] = []; - if (otherLines.length) { - // Split the rest into an array of lines that fit in the viewport - for (let line = 0, i = 0; line < Math.ceil(otherLines.length / this._cols); line++, i += this._cols) { - lines[line] = otherLines.slice(i, i + this._cols); + const firstLine = charLines[0].slice(0, spaceOnCurrentLine); + charLines[0] = charLines[0].slice(spaceOnCurrentLine); + // Split the rest into an array of lines that fit in the viewport + const lines = charLines.flatMap((line, idx) => { + if (idx == charLines.length - 1 && line == "") { + // Add a blank "line" to move the cursor to the next viewport row + return [""]; } - } else { - // Add a blank "line" to move the cursor to the next viewport row - lines.push(""); - } + const chunks = []; + for (let i = 0; i < line.length; i += this._cols) { + chunks.push(line.slice(i, i + this._cols)); + } + return chunks; + }); // Join the lines with the cursor escape code lines.unshift(firstLine); char = lines.join("\r\n"); @@ -678,13 +689,14 @@ class WebSocketTerminal implements vscode.Pseudoterminal { // Send the input to the server for processing this._socket.send(JSON.stringify({ type: this._state, input: this._input })); if (this._state == "prompt") { - this._hideCursorWrite(`\x1b]633;E;${this._inputEscaped()}\x07\x1b]633;C\x07\r\n`); + this._hideCursorWrite(`\x1b]633;E;${this._inputEscaped()};${this._nonce}\x07\r\n\x1b]633;C\x07`); if (this._input == "") { this._promptExitCode = ""; } } this._input = ""; this._state = "eval"; + this._margin = this._cursorCol = 0; } else if (this._input != "" && this._state == "prompt" && this._syntaxColoringEnabled()) { // Syntax color input this._socket.send(JSON.stringify({ type: "color", input: this._input })); @@ -747,6 +759,7 @@ function terminalConfigForUri( } sendLiteTerminalTelemetryEvent(throwErrors ? "profile" : "command"); + const nonce = crypto.randomUUID(); return { name: api.config.serverName && api.config.serverName != "" ? api.config.serverName : "iris", location: @@ -756,9 +769,10 @@ function terminalConfigForUri( vscode.window.terminals.length > 0 ? vscode.TerminalLocation.Editor : vscode.TerminalLocation.Panel, - pty: new WebSocketTerminal(targetUri, nsOverride), + pty: new WebSocketTerminal(targetUri, nonce, nsOverride), isTransient: true, iconPath: iscIcon, + shellIntegrationNonce: nonce, }; }