Skip to content

Commit e6926b9

Browse files
committed
Race terminal shell integration
1 parent d0471ae commit e6926b9

File tree

3 files changed

+136
-8
lines changed

3 files changed

+136
-8
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"Other"
1313
],
1414
"enabledApiProposals": [
15-
"terminalShellEnv"
15+
"terminalShellEnv",
16+
"terminalDataWriteEvent"
1617
],
1718
"capabilities": {
1819
"untrustedWorkspaces": {

src/features/terminal/utils.ts

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,115 @@
11
import * as path from 'path';
2-
import { Terminal, TerminalOptions, Uri } from 'vscode';
2+
import { Terminal, TerminalOptions, Uri, window } from 'vscode';
33
import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api';
4-
import { sleep } from '../../common/utils/asyncUtils';
54
import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis';
65

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

9+
/**
10+
* This function races three conditions:
11+
* 1. Timeout based on VS Code's terminal.integrated.shellIntegration.timeout setting
12+
* 2. Shell integration becoming available (window.onDidChangeTerminalShellIntegration event)
13+
* 3. Detection of common prompt patterns in terminal output
14+
*/
1015
export async function waitForShellIntegration(terminal: Terminal): Promise<boolean> {
11-
let timeout = 0;
12-
while (!terminal.shellIntegration && timeout < SHELL_INTEGRATION_TIMEOUT) {
13-
await sleep(SHELL_INTEGRATION_POLL_INTERVAL);
14-
timeout += SHELL_INTEGRATION_POLL_INTERVAL;
16+
if (terminal.shellIntegration) {
17+
return true;
1518
}
16-
return terminal.shellIntegration !== undefined;
19+
20+
const config = getConfiguration('terminal.integrated');
21+
const timeoutValue = config.get<number | undefined>('shellIntegration.timeout');
22+
const timeoutMs = timeoutValue === undefined || -1 ? 5000 : timeoutValue;
23+
24+
const disposables: { dispose(): void }[] = [];
25+
26+
try {
27+
const result = await Promise.race([
28+
// // Condition 1: Shell integration timeout setting
29+
// new Promise<boolean>((resolve) => {
30+
// setTimeout(() => resolve(false), timeoutMs);
31+
// }),
32+
33+
// // Condition 2: Shell integration becomes available
34+
// new Promise<boolean>((resolve) => {
35+
// disposables.push(
36+
// onDidChangeTerminalShellIntegration((e) => {
37+
// if (e.terminal === terminal) {
38+
// resolve(true);
39+
// }
40+
// }),
41+
// );
42+
// }),
43+
44+
// Condition 3: Detect prompt patterns in terminal output
45+
new Promise<boolean>((resolve) => {
46+
let dataSoFar = '';
47+
disposables.push(
48+
window.onDidWriteTerminalData((e) => {
49+
if (e.terminal === terminal) {
50+
dataSoFar += e.data;
51+
// TODO: Double check the regex.
52+
const lines = dataSoFar.split(/\r?\n/);
53+
const lastNonEmptyLine = lines.filter((line) => line.trim().length > 0).pop();
54+
55+
if (lastNonEmptyLine && detectsCommonPromptPattern(lastNonEmptyLine)) {
56+
resolve(false);
57+
}
58+
}
59+
}),
60+
);
61+
}),
62+
]);
63+
64+
return result;
65+
} finally {
66+
disposables.forEach((d) => d.dispose());
67+
}
68+
}
69+
70+
/**
71+
* Detects if the given text content appears to end with a common prompt pattern.
72+
*
73+
* @param cursorLine The line to check for prompt patterns
74+
* @returns boolean indicating if a prompt pattern was detected
75+
*/
76+
function detectsCommonPromptPattern(cursorLine: string): boolean {
77+
// PowerShell prompt: PS C:\> or similar patterns
78+
if (/PS\s+[A-Z]:\\.*>\s*$/.test(cursorLine)) {
79+
return true;
80+
}
81+
82+
// Command Prompt: C:\path>
83+
if (/^[A-Z]:\\.*>\s*$/.test(cursorLine)) {
84+
return true;
85+
}
86+
87+
// Bash-style prompts ending with $
88+
if (/\$\s*$/.test(cursorLine)) {
89+
return true;
90+
}
91+
92+
// Root prompts ending with #
93+
if (/#\s*$/.test(cursorLine)) {
94+
return true;
95+
}
96+
97+
// Python REPL prompt
98+
if (/^>>>\s*$/.test(cursorLine)) {
99+
return true;
100+
}
101+
102+
// Custom prompts ending with the starship character (\u276f)
103+
if (/\u276f\s*$/.test(cursorLine)) {
104+
return true;
105+
}
106+
107+
// Generic prompts ending with common prompt characters
108+
if (/[>%]\s*$/.test(cursorLine)) {
109+
return true;
110+
}
111+
112+
return false;
17113
}
18114

19115
export function isTaskTerminal(terminal: Terminal): boolean {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
declare module 'vscode' {
7+
// https://github.com/microsoft/vscode/issues/78502
8+
//
9+
// This API is still proposed but we don't intent on promoting it to stable due to problems
10+
// around performance. See #145234 for a more likely API to get stabilized.
11+
12+
export interface TerminalDataWriteEvent {
13+
/**
14+
* The {@link Terminal} for which the data was written.
15+
*/
16+
readonly terminal: Terminal;
17+
/**
18+
* The data being written.
19+
*/
20+
readonly data: string;
21+
}
22+
23+
namespace window {
24+
/**
25+
* An event which fires when the terminal's child pseudo-device is written to (the shell).
26+
* In other words, this provides access to the raw data stream from the process running
27+
* within the terminal, including VT sequences.
28+
*/
29+
export const onDidWriteTerminalData: Event<TerminalDataWriteEvent>;
30+
}
31+
}

0 commit comments

Comments
 (0)