Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e6926b9
Race terminal shell integration
anthonykim1 Oct 24, 2025
f196dd1
Proper disposables
anthonykim1 Oct 24, 2025
7aa0d38
onDidWriteTerminalData should live in window.apis.ts for extension
anthonykim1 Oct 24, 2025
245df55
remove $ from regex
anthonykim1 Oct 24, 2025
f0f4f17
pin vscode version to have new timeout setting from shellIntegration
anthonykim1 Oct 24, 2025
ef2c22b
Try to follow core syntax for timeout
anthonykim1 Oct 24, 2025
4972322
TODO for post-universe
anthonykim1 Oct 24, 2025
1fb26bc
Replace cursorLine -> terminalData
anthonykim1 Oct 24, 2025
b4579f3
Debounce detectCommonPromptPattern to 50ms
anthonykim1 Oct 24, 2025
a3feed7
Use removeAnsiEscapeCodes and bring back $ in prompt regex
anthonykim1 Oct 24, 2025
b3f3359
Don't update vscode engine version
anthonykim1 Oct 24, 2025
d6626fb
Potentially save some compute before calculating regex
anthonykim1 Oct 24, 2025
dc221fa
Tweak the wording for waitForShellIntegration description
anthonykim1 Oct 24, 2025
b7ebab6
Update src/features/terminal/utils.ts
anthonykim1 Oct 25, 2025
f64b62c
Rename sleep to timeout for better wording consistency
anthonykim1 Oct 25, 2025
2b56b82
add missing [] next to string to make it stringbuilder
anthonykim1 Oct 25, 2025
73f3f3c
Try to make SimpleDebounce disposable
anthonykim1 Oct 25, 2025
28f5261
make timeout setting more consistent with vscode
anthonykim1 Oct 25, 2025
86141c3
Revert "Try to make SimpleDebounce disposable"
anthonykim1 Oct 25, 2025
cca8bbb
Make SimpleDebounce disposable
anthonykim1 Oct 26, 2025
6b589d5
remove unecessary, make it clean for review
anthonykim1 Oct 26, 2025
ff91421
Update src/common/utils/asyncUtils.ts
Tyriar Oct 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"Other"
],
"enabledApiProposals": [
"terminalShellEnv"
"terminalShellEnv",
"terminalDataWriteEvent"
],
"capabilities": {
"untrustedWorkspaces": {
Expand Down
6 changes: 6 additions & 0 deletions src/common/utils/asyncUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export async function sleep(milliseconds: number) {
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
}

export function timeout(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

// TODO: Advanced timeout from core async: https://github.com/microsoft/vscode-python-environments/issues/953
8 changes: 8 additions & 0 deletions src/common/window.apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uri[] | undefined> {
return window.showOpenDialog(options);
}
Expand Down
135 changes: 128 additions & 7 deletions src/features/terminal/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,115 @@
import * as path from 'path';
import { Terminal, TerminalOptions, Uri } from 'vscode';
import { Disposable, Terminal, TerminalOptions, Uri } from 'vscode';
import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api';
import { sleep } from '../../common/utils/asyncUtils';
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';

export const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds
export const SHELL_INTEGRATION_POLL_INTERVAL = 20; // 0.02 seconds

/**
* 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
*/
export async function waitForShellIntegration(terminal: Terminal): Promise<boolean> {
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;
}

const config = getConfiguration('terminal.integrated');
const timeoutValue = config.get<number | undefined>('shellIntegration.timeout');
const timeoutMs = timeoutValue === undefined || -1 ? 5000 : timeoutValue;

const disposables: Disposable[] = [];

try {
const result = await Promise.race([
// Condition 1: Shell integration timeout setting
timeout(timeoutMs).then(() => false),

// Condition 2: Shell integration becomes available
new Promise<boolean>((resolve) => {
disposables.push(
onDidChangeTerminalShellIntegration((e) => {
if (e.terminal === terminal) {
resolve(true);
}
}),
);
}),

// Condition 3: Detect prompt patterns in terminal output
new Promise<boolean>((resolve) => {
let dataSoFar = '';
const debounced = createSimpleDebounce(50, () => {
if (dataSoFar && detectsCommonPromptPattern(dataSoFar)) {
resolve(false);
}
});
disposables.push(
onDidWriteTerminalData((e) => {
if (e.terminal === terminal) {
dataSoFar += e.data;
debounced.trigger();
}
}),
);
}),
]);

return result;
} finally {
disposables.forEach((d) => d.dispose());
}
return terminal.shellIntegration !== undefined;
}

// Detects if the given text content appears to end with a common prompt pattern.
function detectsCommonPromptPattern(terminalData: string): boolean {
if (terminalData.trim().length === 0) {
return false;
}

const sanitizedTerminalData = removeAnsiEscapeCodes(terminalData);
// PowerShell prompt: PS C:\> or similar patterns
if (/PS\s+[A-Z]:\\.*>\s*$/.test(sanitizedTerminalData)) {
return true;
}

// Command Prompt: C:\path>
if (/^[A-Z]:\\.*>\s*$/.test(sanitizedTerminalData)) {
return true;
}

// Bash-style prompts ending with $
if (/\$\s*$/.test(sanitizedTerminalData)) {
return true;
}

// Root prompts ending with #
if (/#\s*$/.test(sanitizedTerminalData)) {
return true;
}

// Python REPL prompt
if (/^>>>\s*$/.test(sanitizedTerminalData)) {
return true;
}

// Custom prompts ending with the starship character (\u276f)
if (/\u276f\s*$/.test(sanitizedTerminalData)) {
return true;
}

// Generic prompts ending with common prompt characters
if (/[>%]\s*$/.test(sanitizedTerminalData)) {
return true;
}

return false;
}

export function isTaskTerminal(terminal: Terminal): boolean {
Expand Down Expand Up @@ -171,3 +267,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;
}
31 changes: 31 additions & 0 deletions src/vscode.proposed.terminalDataWriteEvent.d.ts
Original file line number Diff line number Diff line change
@@ -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<TerminalDataWriteEvent>;
}
}