From e6926b98948aae574986389815a068a9b2e61a64 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 10:18:14 -0700 Subject: [PATCH 01/22] Race terminal shell integration --- package.json | 3 +- src/features/terminal/utils.ts | 110 ++++++++++++++++-- ...scode.proposed.terminalDataWriteEvent.d.ts | 31 +++++ 3 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 src/vscode.proposed.terminalDataWriteEvent.d.ts diff --git a/package.json b/package.json index 0ae1c9a7..46a8e66d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "Other" ], "enabledApiProposals": [ - "terminalShellEnv" + "terminalShellEnv", + "terminalDataWriteEvent" ], "capabilities": { "untrustedWorkspaces": { diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index c8f6443d..2e8c0992 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -1,19 +1,115 @@ import * as path from 'path'; -import { Terminal, TerminalOptions, Uri } from 'vscode'; +import { Terminal, TerminalOptions, Uri, window } from 'vscode'; import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api'; -import { sleep } from '../../common/utils/asyncUtils'; import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis'; export const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds export const SHELL_INTEGRATION_POLL_INTERVAL = 20; // 0.02 seconds +/** + * This function races three conditions: + * 1. Timeout based on VS Code's terminal.integrated.shellIntegration.timeout setting + * 2. Shell integration becoming available (window.onDidChangeTerminalShellIntegration event) + * 3. Detection of common prompt patterns in terminal output + */ export async function waitForShellIntegration(terminal: Terminal): Promise { - let timeout = 0; - while (!terminal.shellIntegration && timeout < SHELL_INTEGRATION_TIMEOUT) { - await sleep(SHELL_INTEGRATION_POLL_INTERVAL); - timeout += SHELL_INTEGRATION_POLL_INTERVAL; + if (terminal.shellIntegration) { + return true; } - return terminal.shellIntegration !== undefined; + + const config = getConfiguration('terminal.integrated'); + const timeoutValue = config.get('shellIntegration.timeout'); + const timeoutMs = timeoutValue === undefined || -1 ? 5000 : timeoutValue; + + const disposables: { dispose(): void }[] = []; + + try { + const result = await Promise.race([ + // // Condition 1: Shell integration timeout setting + // new Promise((resolve) => { + // setTimeout(() => resolve(false), timeoutMs); + // }), + + // // Condition 2: Shell integration becomes available + // new Promise((resolve) => { + // disposables.push( + // onDidChangeTerminalShellIntegration((e) => { + // if (e.terminal === terminal) { + // resolve(true); + // } + // }), + // ); + // }), + + // Condition 3: Detect prompt patterns in terminal output + new Promise((resolve) => { + let dataSoFar = ''; + disposables.push( + window.onDidWriteTerminalData((e) => { + if (e.terminal === terminal) { + dataSoFar += e.data; + // TODO: Double check the regex. + const lines = dataSoFar.split(/\r?\n/); + const lastNonEmptyLine = lines.filter((line) => line.trim().length > 0).pop(); + + if (lastNonEmptyLine && detectsCommonPromptPattern(lastNonEmptyLine)) { + resolve(false); + } + } + }), + ); + }), + ]); + + return result; + } finally { + disposables.forEach((d) => d.dispose()); + } +} + +/** + * Detects if the given text content appears to end with a common prompt pattern. + * + * @param cursorLine The line to check for prompt patterns + * @returns boolean indicating if a prompt pattern was detected + */ +function detectsCommonPromptPattern(cursorLine: string): boolean { + // PowerShell prompt: PS C:\> or similar patterns + if (/PS\s+[A-Z]:\\.*>\s*$/.test(cursorLine)) { + return true; + } + + // Command Prompt: C:\path> + if (/^[A-Z]:\\.*>\s*$/.test(cursorLine)) { + return true; + } + + // Bash-style prompts ending with $ + if (/\$\s*$/.test(cursorLine)) { + return true; + } + + // Root prompts ending with # + if (/#\s*$/.test(cursorLine)) { + return true; + } + + // Python REPL prompt + if (/^>>>\s*$/.test(cursorLine)) { + return true; + } + + // Custom prompts ending with the starship character (\u276f) + if (/\u276f\s*$/.test(cursorLine)) { + return true; + } + + // Generic prompts ending with common prompt characters + if (/[>%]\s*$/.test(cursorLine)) { + return true; + } + + return false; } export function isTaskTerminal(terminal: Terminal): boolean { diff --git a/src/vscode.proposed.terminalDataWriteEvent.d.ts b/src/vscode.proposed.terminalDataWriteEvent.d.ts new file mode 100644 index 00000000..6913b862 --- /dev/null +++ b/src/vscode.proposed.terminalDataWriteEvent.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/78502 + // + // This API is still proposed but we don't intent on promoting it to stable due to problems + // around performance. See #145234 for a more likely API to get stabilized. + + export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; + } + + namespace window { + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + export const onDidWriteTerminalData: Event; + } +} From f196dd11da68b59e9522b016632e7078cd32e66f Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 10:39:19 -0700 Subject: [PATCH 02/22] Proper disposables --- src/features/terminal/utils.ts | 37 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 2e8c0992..9b166d5d 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -1,6 +1,7 @@ import * as path from 'path'; -import { Terminal, TerminalOptions, Uri, window } from 'vscode'; +import { Disposable, Terminal, TerminalOptions, Uri, window } from 'vscode'; import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api'; +import { onDidChangeTerminalShellIntegration } from '../../common/window.apis'; import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis'; export const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds @@ -21,25 +22,25 @@ export async function waitForShellIntegration(terminal: Terminal): Promise('shellIntegration.timeout'); const timeoutMs = timeoutValue === undefined || -1 ? 5000 : timeoutValue; - const disposables: { dispose(): void }[] = []; + const disposables: Disposable[] = []; try { const result = await Promise.race([ - // // Condition 1: Shell integration timeout setting - // new Promise((resolve) => { - // setTimeout(() => resolve(false), timeoutMs); - // }), - - // // Condition 2: Shell integration becomes available - // new Promise((resolve) => { - // disposables.push( - // onDidChangeTerminalShellIntegration((e) => { - // if (e.terminal === terminal) { - // resolve(true); - // } - // }), - // ); - // }), + // Condition 1: Shell integration timeout setting + new Promise((resolve) => { + setTimeout(() => resolve(false), timeoutMs); + }), + + // Condition 2: Shell integration becomes available + new Promise((resolve) => { + disposables.push( + onDidChangeTerminalShellIntegration((e) => { + if (e.terminal === terminal) { + resolve(true); + } + }), + ); + }), // Condition 3: Detect prompt patterns in terminal output new Promise((resolve) => { @@ -48,10 +49,8 @@ export async function waitForShellIntegration(terminal: Terminal): Promise { if (e.terminal === terminal) { dataSoFar += e.data; - // TODO: Double check the regex. const lines = dataSoFar.split(/\r?\n/); const lastNonEmptyLine = lines.filter((line) => line.trim().length > 0).pop(); - if (lastNonEmptyLine && detectsCommonPromptPattern(lastNonEmptyLine)) { resolve(false); } From 7aa0d38a749ecc1a4edf6a86b767e26b37379241 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 10:48:48 -0700 Subject: [PATCH 03/22] onDidWriteTerminalData should live in window.apis.ts for extension --- src/common/window.apis.ts | 8 ++++++++ src/features/terminal/utils.ts | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/common/window.apis.ts b/src/common/window.apis.ts index 51a20320..26af79d5 100644 --- a/src/common/window.apis.ts +++ b/src/common/window.apis.ts @@ -49,6 +49,14 @@ export function onDidChangeTerminalShellIntegration( return window.onDidChangeTerminalShellIntegration(listener, thisArgs, disposables); } +export function onDidWriteTerminalData( + listener: (e: { readonly terminal: Terminal; readonly data: string }) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return window.onDidWriteTerminalData(listener, thisArgs, disposables); +} + export function showOpenDialog(options?: OpenDialogOptions): Thenable { return window.showOpenDialog(options); } diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 9b166d5d..f7f99e23 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -1,7 +1,7 @@ import * as path from 'path'; -import { Disposable, Terminal, TerminalOptions, Uri, window } from 'vscode'; +import { Disposable, Terminal, TerminalOptions, Uri } from 'vscode'; import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api'; -import { onDidChangeTerminalShellIntegration } from '../../common/window.apis'; +import { onDidChangeTerminalShellIntegration, onDidWriteTerminalData } from '../../common/window.apis'; import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis'; export const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds @@ -46,7 +46,7 @@ export async function waitForShellIntegration(terminal: Terminal): Promise((resolve) => { let dataSoFar = ''; disposables.push( - window.onDidWriteTerminalData((e) => { + onDidWriteTerminalData((e) => { if (e.terminal === terminal) { dataSoFar += e.data; const lines = dataSoFar.split(/\r?\n/); From 245df5553b5cf507ab746f2fb2331fa8f4bab9a6 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 11:04:10 -0700 Subject: [PATCH 04/22] remove $ from regex --- src/features/terminal/utils.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index f7f99e23..25713d93 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -49,9 +49,7 @@ export async function waitForShellIntegration(terminal: Terminal): Promise { if (e.terminal === terminal) { dataSoFar += e.data; - const lines = dataSoFar.split(/\r?\n/); - const lastNonEmptyLine = lines.filter((line) => line.trim().length > 0).pop(); - if (lastNonEmptyLine && detectsCommonPromptPattern(lastNonEmptyLine)) { + if (dataSoFar && detectsCommonPromptPattern(dataSoFar)) { resolve(false); } } @@ -66,45 +64,40 @@ export async function waitForShellIntegration(terminal: Terminal): Promise or similar patterns - if (/PS\s+[A-Z]:\\.*>\s*$/.test(cursorLine)) { + if (/PS\s+[A-Z]:\\.*>\s*/.test(cursorLine)) { return true; } // Command Prompt: C:\path> - if (/^[A-Z]:\\.*>\s*$/.test(cursorLine)) { + if (/^[A-Z]:\\.*>\s*/.test(cursorLine)) { return true; } // Bash-style prompts ending with $ - if (/\$\s*$/.test(cursorLine)) { + if (/\$\s*/.test(cursorLine)) { return true; } // Root prompts ending with # - if (/#\s*$/.test(cursorLine)) { + if (/#\s*/.test(cursorLine)) { return true; } // Python REPL prompt - if (/^>>>\s*$/.test(cursorLine)) { + if (/^>>>\s*/.test(cursorLine)) { return true; } // Custom prompts ending with the starship character (\u276f) - if (/\u276f\s*$/.test(cursorLine)) { + if (/\u276f\s*/.test(cursorLine)) { return true; } // Generic prompts ending with common prompt characters - if (/[>%]\s*$/.test(cursorLine)) { + if (/[>%]\s*/.test(cursorLine)) { return true; } From f0f4f1744ad6cf0dacf3c0962a7bb21fae910024 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 12:06:11 -0700 Subject: [PATCH 05/22] pin vscode version to have new timeout setting from shellIntegration --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b42f9b7d..836c1315 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "webpack-cli": "^5.1.1" }, "engines": { - "vscode": "^1.104.0-20250815" + "vscode": "^1.104.0-20251024" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 46a8e66d..ae277210 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "publisher": "ms-python", "preview": true, "engines": { - "vscode": "^1.104.0-20250815" + "vscode": "^1.104.0-20251024" }, "categories": [ "Other" From ef2c22bd3d1c4f8d6f5637f9cfb42ee0ae093f9e Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 13:14:02 -0700 Subject: [PATCH 06/22] Try to follow core syntax for timeout --- src/common/utils/asyncUtils.ts | 6 ++++++ src/features/terminal/utils.ts | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts index 4bb79f84..bd02f125 100644 --- a/src/common/utils/asyncUtils.ts +++ b/src/common/utils/asyncUtils.ts @@ -1,3 +1,9 @@ export async function sleep(milliseconds: number) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } + +export function timeout(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// TODO: Bring timeouts from VS Code: src/vs/base/common/async.ts diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 25713d93..0461bcc9 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import { Disposable, Terminal, TerminalOptions, Uri } from 'vscode'; import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api'; +import { timeout } from '../../common/utils/asyncUtils'; import { onDidChangeTerminalShellIntegration, onDidWriteTerminalData } from '../../common/window.apis'; import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis'; @@ -27,9 +28,7 @@ export async function waitForShellIntegration(terminal: Terminal): Promise((resolve) => { - setTimeout(() => resolve(false), timeoutMs); - }), + timeout(timeoutMs).then(() => false), // Condition 2: Shell integration becomes available new Promise((resolve) => { From 497232275ce2f1d6208daa6e726145f56ea80bec Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 13:17:32 -0700 Subject: [PATCH 07/22] TODO for post-universe --- src/common/utils/asyncUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts index bd02f125..b8ea35c0 100644 --- a/src/common/utils/asyncUtils.ts +++ b/src/common/utils/asyncUtils.ts @@ -6,4 +6,4 @@ export function timeout(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -// TODO: Bring timeouts from VS Code: src/vs/base/common/async.ts +// TODO: Advanced timeout from core async: https://github.com/microsoft/vscode-python-environments/issues/953 From 1fb26bc38b21b4f3df50dc98b7ea2d46e0321850 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 13:18:54 -0700 Subject: [PATCH 08/22] Replace cursorLine -> terminalData --- src/features/terminal/utils.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 0461bcc9..c25a4d22 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -64,39 +64,39 @@ export async function waitForShellIntegration(terminal: Terminal): Promise or similar patterns - if (/PS\s+[A-Z]:\\.*>\s*/.test(cursorLine)) { + if (/PS\s+[A-Z]:\\.*>\s*/.test(terminalData)) { return true; } // Command Prompt: C:\path> - if (/^[A-Z]:\\.*>\s*/.test(cursorLine)) { + if (/^[A-Z]:\\.*>\s*/.test(terminalData)) { return true; } // Bash-style prompts ending with $ - if (/\$\s*/.test(cursorLine)) { + if (/\$\s*/.test(terminalData)) { return true; } // Root prompts ending with # - if (/#\s*/.test(cursorLine)) { + if (/#\s*/.test(terminalData)) { return true; } // Python REPL prompt - if (/^>>>\s*/.test(cursorLine)) { + if (/^>>>\s*/.test(terminalData)) { return true; } // Custom prompts ending with the starship character (\u276f) - if (/\u276f\s*/.test(cursorLine)) { + if (/\u276f\s*/.test(terminalData)) { return true; } // Generic prompts ending with common prompt characters - if (/[>%]\s*/.test(cursorLine)) { + if (/[>%]\s*/.test(terminalData)) { return true; } From b4579f3455431f9dec5cdbdd0936adb135542ffe Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 13:23:09 -0700 Subject: [PATCH 09/22] Debounce detectCommonPromptPattern to 50ms --- src/features/terminal/utils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index c25a4d22..a88758f6 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { Disposable, Terminal, TerminalOptions, Uri } from 'vscode'; import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api'; import { timeout } from '../../common/utils/asyncUtils'; +import { createSimpleDebounce } from '../../common/utils/debounce'; import { onDidChangeTerminalShellIntegration, onDidWriteTerminalData } from '../../common/window.apis'; import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis'; @@ -44,13 +45,16 @@ export async function waitForShellIntegration(terminal: Terminal): Promise((resolve) => { let dataSoFar = ''; + const debounced = createSimpleDebounce(50, () => { + if (dataSoFar && detectsCommonPromptPattern(dataSoFar)) { + resolve(false); + } + }); disposables.push( onDidWriteTerminalData((e) => { if (e.terminal === terminal) { dataSoFar += e.data; - if (dataSoFar && detectsCommonPromptPattern(dataSoFar)) { - resolve(false); - } + debounced.trigger(); } }), ); From a3feed7c10fb300abedcdfd7aab731d14b25103b Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 13:32:44 -0700 Subject: [PATCH 10/22] Use removeAnsiEscapeCodes and bring back $ in prompt regex --- src/features/terminal/utils.ts | 40 ++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index a88758f6..89fb3095 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -69,38 +69,39 @@ export async function waitForShellIntegration(terminal: Terminal): Promise or similar patterns - if (/PS\s+[A-Z]:\\.*>\s*/.test(terminalData)) { + if (/PS\s+[A-Z]:\\.*>\s*$/.test(sanitizedTerminalData)) { return true; } // Command Prompt: C:\path> - if (/^[A-Z]:\\.*>\s*/.test(terminalData)) { + if (/^[A-Z]:\\.*>\s*$/.test(sanitizedTerminalData)) { return true; } // Bash-style prompts ending with $ - if (/\$\s*/.test(terminalData)) { + if (/\$\s*$/.test(sanitizedTerminalData)) { return true; } // Root prompts ending with # - if (/#\s*/.test(terminalData)) { + if (/#\s*$/.test(sanitizedTerminalData)) { return true; } // Python REPL prompt - if (/^>>>\s*/.test(terminalData)) { + if (/^>>>\s*$/.test(sanitizedTerminalData)) { return true; } // Custom prompts ending with the starship character (\u276f) - if (/\u276f\s*/.test(terminalData)) { + if (/\u276f\s*$/.test(sanitizedTerminalData)) { return true; } // Generic prompts ending with common prompt characters - if (/[>%]\s*/.test(terminalData)) { + if (/[>%]\s*$/.test(sanitizedTerminalData)) { return true; } @@ -262,3 +263,28 @@ export async function getAllDistinctProjectEnvironments( return envs.length > 0 ? envs : undefined; } + +// Defacto standard: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html +const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/; +const OSC_SEQUENCE = /(?:\x1b\]|\x9d).*?(?:\x1b\\|\x07|\x9c)/; +const ESC_SEQUENCE = /\x1b(?:[ #%\(\)\*\+\-\.\/]?[a-zA-Z0-9\|}~@])/; +const CONTROL_SEQUENCES = new RegExp( + '(?:' + [CSI_SEQUENCE.source, OSC_SEQUENCE.source, ESC_SEQUENCE.source].join('|') + ')', + 'g', +); + +/** + * Strips ANSI escape sequences from a string. + * @param str The dastringa stringo strip the ANSI escape sequences from. + * + * @example + * removeAnsiEscapeCodes('\u001b[31mHello, World!\u001b[0m'); + * // 'Hello, World!' + */ +export function removeAnsiEscapeCodes(str: string): string { + if (str) { + str = str.replace(CONTROL_SEQUENCES, ''); + } + + return str; +} From b3f3359a3d6be5b7b8aac5e89dcf45d4cc4d7187 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 13:41:44 -0700 Subject: [PATCH 11/22] Don't update vscode engine version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 836c1315..b42f9b7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "webpack-cli": "^5.1.1" }, "engines": { - "vscode": "^1.104.0-20251024" + "vscode": "^1.104.0-20250815" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index ae277210..46a8e66d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "publisher": "ms-python", "preview": true, "engines": { - "vscode": "^1.104.0-20251024" + "vscode": "^1.104.0-20250815" }, "categories": [ "Other" From d6626fb6aca7224c7b17daea1a072b577fe54e25 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 13:51:36 -0700 Subject: [PATCH 12/22] Potentially save some compute before calculating regex --- src/features/terminal/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 89fb3095..a22771f8 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -69,6 +69,10 @@ export async function waitForShellIntegration(terminal: Terminal): Promise or similar patterns if (/PS\s+[A-Z]:\\.*>\s*$/.test(sanitizedTerminalData)) { From dc221fa8654be6988fc80d1e71313da69e7f5f00 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Fri, 24 Oct 2025 14:26:47 -0700 Subject: [PATCH 13/22] Tweak the wording for waitForShellIntegration description --- src/features/terminal/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index a22771f8..52ed1387 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -10,7 +10,7 @@ export const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds export const SHELL_INTEGRATION_POLL_INTERVAL = 20; // 0.02 seconds /** - * This function races three conditions: + * Three conditions in a Promise.race: * 1. Timeout based on VS Code's terminal.integrated.shellIntegration.timeout setting * 2. Shell integration becoming available (window.onDidChangeTerminalShellIntegration event) * 3. Detection of common prompt patterns in terminal output From b7ebab69d88cf1bcde8564a3c2b5e83c39425858 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Sat, 25 Oct 2025 09:26:01 -0700 Subject: [PATCH 14/22] Update src/features/terminal/utils.ts Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- src/features/terminal/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 52ed1387..39e726a5 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -44,16 +44,16 @@ export async function waitForShellIntegration(terminal: Terminal): Promise((resolve) => { - let dataSoFar = ''; + const dataEvents: string = []; const debounced = createSimpleDebounce(50, () => { - if (dataSoFar && detectsCommonPromptPattern(dataSoFar)) { + if (dataEvents && detectsCommonPromptPattern(dataEvents.join(''))) { resolve(false); } }); disposables.push( onDidWriteTerminalData((e) => { if (e.terminal === terminal) { - dataSoFar += e.data; + dataEvents.push(e.data); debounced.trigger(); } }), From f64b62cb44db0b983d3720eecc366573a91d394e Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Sat, 25 Oct 2025 09:57:15 -0700 Subject: [PATCH 15/22] Rename sleep to timeout for better wording consistency --- src/common/utils/asyncUtils.ts | 6 +----- .../terminal/shells/common/shellUtils.ts | 10 +++++----- .../creators/autoFindProjects.unit.test.ts | 18 +++++++++--------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts index b8ea35c0..bfaadcbc 100644 --- a/src/common/utils/asyncUtils.ts +++ b/src/common/utils/asyncUtils.ts @@ -1,9 +1,5 @@ -export async function sleep(milliseconds: number) { +export async function timeout(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } -export function timeout(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - // TODO: Advanced timeout from core async: https://github.com/microsoft/vscode-python-environments/issues/953 diff --git a/src/features/terminal/shells/common/shellUtils.ts b/src/features/terminal/shells/common/shellUtils.ts index 381be880..0d74e31e 100644 --- a/src/features/terminal/shells/common/shellUtils.ts +++ b/src/features/terminal/shells/common/shellUtils.ts @@ -1,7 +1,7 @@ import { PythonCommandRunConfiguration, PythonEnvironment } from '../../../../api'; import { traceInfo } from '../../../../common/logging'; import { getGlobalPersistentState } from '../../../../common/persistentState'; -import { sleep } from '../../../../common/utils/asyncUtils'; +import { timeout } from '../../../../common/utils/asyncUtils'; import { isWindows } from '../../../../common/utils/platformUtils'; import { activeTerminalShellIntegration } from '../../../../common/window.apis'; import { getConfiguration } from '../../../../common/workspace.apis'; @@ -106,11 +106,11 @@ export function extractProfilePath(content: string): string | undefined { export async function shellIntegrationForActiveTerminal(name: string, profile?: string): Promise { let hasShellIntegration = activeTerminalShellIntegration(); - let timeout = 0; + let timeOutstamp = 0; - while (!hasShellIntegration && timeout < SHELL_INTEGRATION_TIMEOUT) { - await sleep(SHELL_INTEGRATION_POLL_INTERVAL); - timeout += SHELL_INTEGRATION_POLL_INTERVAL; + while (!hasShellIntegration && timeOutstamp < SHELL_INTEGRATION_TIMEOUT) { + await timeout(SHELL_INTEGRATION_POLL_INTERVAL); + timeOutstamp += SHELL_INTEGRATION_POLL_INTERVAL; hasShellIntegration = activeTerminalShellIntegration(); } diff --git a/src/test/features/creators/autoFindProjects.unit.test.ts b/src/test/features/creators/autoFindProjects.unit.test.ts index 269aaa99..b1ad82a7 100644 --- a/src/test/features/creators/autoFindProjects.unit.test.ts +++ b/src/test/features/creators/autoFindProjects.unit.test.ts @@ -1,15 +1,15 @@ +import assert from 'assert'; import * as path from 'path'; import * as sinon from 'sinon'; import * as typmoq from 'typemoq'; -import * as wapi from '../../../common/workspace.apis'; -import * as winapi from '../../../common/window.apis'; -import { PythonProjectManager } from '../../../internal.api'; -import { createDeferred } from '../../../common/utils/deferred'; -import { AutoFindProjects } from '../../../features/creators/autoFindProjects'; -import assert from 'assert'; import { Uri } from 'vscode'; import { PythonProject } from '../../../api'; -import { sleep } from '../../../common/utils/asyncUtils'; +import { timeout } from '../../../common/utils/asyncUtils'; +import { createDeferred } from '../../../common/utils/deferred'; +import * as winapi from '../../../common/window.apis'; +import * as wapi from '../../../common/workspace.apis'; +import { AutoFindProjects } from '../../../features/creators/autoFindProjects'; +import { PythonProjectManager } from '../../../internal.api'; suite('Auto Find Project tests', () => { let findFilesStub: sinon.SinonStub; @@ -45,7 +45,7 @@ suite('Auto Find Project tests', () => { const result = await autoFindProjects.create(); assert.equal(result, undefined, 'Result should be undefined'); - await Promise.race([deferred.promise, sleep(100)]); + await Promise.race([deferred.promise, timeout(100)]); assert.ok(errorShown, 'Error message should have been shown'); }); @@ -64,7 +64,7 @@ suite('Auto Find Project tests', () => { const result = await autoFindProjects.create(); assert.equal(result, undefined, 'Result should be undefined'); - await Promise.race([deferred.promise, sleep(100)]); + await Promise.race([deferred.promise, timeout(100)]); assert.ok(errorShown, 'Error message should have been shown'); }); From 2b56b82f17d7f8b7f3bb0f978814dc0179c5c646 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Sat, 25 Oct 2025 10:02:44 -0700 Subject: [PATCH 16/22] add missing [] next to string to make it stringbuilder --- src/features/terminal/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 39e726a5..1954a8af 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -44,7 +44,7 @@ export async function waitForShellIntegration(terminal: Terminal): Promise((resolve) => { - const dataEvents: string = []; + const dataEvents: string[] = []; const debounced = createSimpleDebounce(50, () => { if (dataEvents && detectsCommonPromptPattern(dataEvents.join(''))) { resolve(false); From 73f3f3c9ee554339d262edc41fa83a1c2e028dac Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Sat, 25 Oct 2025 10:10:38 -0700 Subject: [PATCH 17/22] Try to make SimpleDebounce disposable --- src/common/utils/debounce.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/common/utils/debounce.ts b/src/common/utils/debounce.ts index 1b197063..40f673a5 100644 --- a/src/common/utils/debounce.ts +++ b/src/common/utils/debounce.ts @@ -1,11 +1,20 @@ -export interface SimpleDebounce { +import { Disposable } from 'vscode'; + +export interface SimpleDebounce extends Disposable { trigger(): void; } -class SimpleDebounceImpl { +class SimpleDebounceImpl extends Disposable { private timeout: NodeJS.Timeout | undefined; - constructor(private readonly ms: number, private readonly callback: () => void) {} + constructor(private readonly ms: number, private readonly callback: () => void) { + super(() => { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + }); + } public trigger() { if (this.timeout) { @@ -15,6 +24,12 @@ class SimpleDebounceImpl { this.callback(); }, this.ms); } + + public dispose() { + if (this.timeout) { + clearTimeout(this.timeout); + } + } } export function createSimpleDebounce(ms: number, callback: () => void): SimpleDebounce { From 28f5261d294977bc53cd252d01b478db291914cc Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Sat, 25 Oct 2025 10:31:40 -0700 Subject: [PATCH 18/22] make timeout setting more consistent with vscode --- src/features/terminal/utils.ts | 11 +++++++++-- src/managers/common/utils.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index 1954a8af..a691b507 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { Disposable, Terminal, TerminalOptions, Uri } from 'vscode'; +import { Disposable, env, Terminal, TerminalOptions, Uri } from 'vscode'; import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api'; import { timeout } from '../../common/utils/asyncUtils'; import { createSimpleDebounce } from '../../common/utils/debounce'; @@ -21,8 +21,15 @@ export async function waitForShellIntegration(terminal: Terminal): Promise('shellIntegration.enabled', true); const timeoutValue = config.get('shellIntegration.timeout'); - const timeoutMs = timeoutValue === undefined || -1 ? 5000 : timeoutValue; + const isRemote = env.remoteName !== undefined; + let timeoutMs: number; + if (typeof timeoutValue !== 'number' || timeoutValue < 0) { + timeoutMs = shellIntegrationEnabled ? 5000 : isRemote ? 3000 : 2000; + } else { + timeoutMs = Math.max(timeoutValue, 500); + } const disposables: Disposable[] = []; diff --git a/src/managers/common/utils.ts b/src/managers/common/utils.ts index bffa42af..8d2e9494 100644 --- a/src/managers/common/utils.ts +++ b/src/managers/common/utils.ts @@ -13,6 +13,14 @@ export function noop() { // do nothing } +/** + * In **contrast** to just checking `typeof` this will return `false` for `NaN`. + * @returns whether the provided parameter is a JavaScript Number or not. + */ +export function isNumber(obj: unknown): obj is number { + return typeof obj === 'number' && !isNaN(obj); +} + export function shortVersion(version: string): string { const pattern = /(\d)\.(\d+)(?:\.(\d+)?)?/gm; const match = pattern.exec(version); From 86141c31d1b783a086d81a6b055be75c8f26287a Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Sat, 25 Oct 2025 10:44:30 -0700 Subject: [PATCH 19/22] Revert "Try to make SimpleDebounce disposable" This reverts commit 73f3f3c9ee554339d262edc41fa83a1c2e028dac. --- src/common/utils/debounce.ts | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/common/utils/debounce.ts b/src/common/utils/debounce.ts index 40f673a5..1b197063 100644 --- a/src/common/utils/debounce.ts +++ b/src/common/utils/debounce.ts @@ -1,20 +1,11 @@ -import { Disposable } from 'vscode'; - -export interface SimpleDebounce extends Disposable { +export interface SimpleDebounce { trigger(): void; } -class SimpleDebounceImpl extends Disposable { +class SimpleDebounceImpl { private timeout: NodeJS.Timeout | undefined; - constructor(private readonly ms: number, private readonly callback: () => void) { - super(() => { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = undefined; - } - }); - } + constructor(private readonly ms: number, private readonly callback: () => void) {} public trigger() { if (this.timeout) { @@ -24,12 +15,6 @@ class SimpleDebounceImpl extends Disposable { this.callback(); }, this.ms); } - - public dispose() { - if (this.timeout) { - clearTimeout(this.timeout); - } - } } export function createSimpleDebounce(ms: number, callback: () => void): SimpleDebounce { From cca8bbbe499cda2483b37f21ab62ab038dfb8445 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Sat, 25 Oct 2025 17:53:38 -0700 Subject: [PATCH 20/22] Make SimpleDebounce disposable --- src/common/utils/debounce.ts | 14 +++++++++++--- src/features/terminal/utils.ts | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/common/utils/debounce.ts b/src/common/utils/debounce.ts index 1b197063..ed8858ad 100644 --- a/src/common/utils/debounce.ts +++ b/src/common/utils/debounce.ts @@ -1,13 +1,14 @@ -export interface SimpleDebounce { +import { Disposable } from 'vscode'; +export interface SimpleDebounce extends Disposable { trigger(): void; } -class SimpleDebounceImpl { +class SimpleDebounceImpl implements SimpleDebounce { private timeout: NodeJS.Timeout | undefined; constructor(private readonly ms: number, private readonly callback: () => void) {} - public trigger() { + public trigger(): void { if (this.timeout) { clearTimeout(this.timeout); } @@ -15,6 +16,13 @@ class SimpleDebounceImpl { this.callback(); }, this.ms); } + + public dispose(): void { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + } } export function createSimpleDebounce(ms: number, callback: () => void): SimpleDebounce { diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index a691b507..bca60e08 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -57,6 +57,7 @@ export async function waitForShellIntegration(terminal: Terminal): Promise { if (e.terminal === terminal) { From 6b589d5cfe105585bccc329bb0c540051930d181 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Sat, 25 Oct 2025 17:54:54 -0700 Subject: [PATCH 21/22] remove unecessary, make it clean for review --- src/common/utils/debounce.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/utils/debounce.ts b/src/common/utils/debounce.ts index ed8858ad..3885e00f 100644 --- a/src/common/utils/debounce.ts +++ b/src/common/utils/debounce.ts @@ -20,7 +20,6 @@ class SimpleDebounceImpl implements SimpleDebounce { public dispose(): void { if (this.timeout) { clearTimeout(this.timeout); - this.timeout = undefined; } } } From ff91421af3020e40f98086b549817add3246af8a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 26 Oct 2025 02:44:50 -0700 Subject: [PATCH 22/22] Update src/common/utils/asyncUtils.ts --- src/common/utils/asyncUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts index bfaadcbc..02bb265c 100644 --- a/src/common/utils/asyncUtils.ts +++ b/src/common/utils/asyncUtils.ts @@ -1,5 +1,3 @@ export async function timeout(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } - -// TODO: Advanced timeout from core async: https://github.com/microsoft/vscode-python-environments/issues/953