Skip to content

Commit c8d26d9

Browse files
authored
fix(terminal): wait for shell integration before running Python commands (#1741)
## Summary - Wait for shell integration before selecting the `sendText` fallback in `runInTerminal`. - Re-check `terminal.shellIntegration` after the wait so a terminal that becomes ready uses `executeCommand`. - Add unit-test coverage for the ordering. ## Background On a newly created terminal, `terminal.shellIntegration` can be undefined while the shell is still initializing. `runInTerminal` previously selected the `sendText` fallback immediately in that state. With Fish, the command can then be written before the shell prompt and shell-integration sequences are ready, which can produce duplicate command or prompt echo. The existing `waitForShellIntegration` helper already filters shell-integration events to the target terminal. This change waits for that helper before selecting the execution path and preserves the existing `sendText` fallback when shell integration remains unavailable. Related to #1312 ## Scope This PR addresses the early `sendText` race in the Python Environments extension. A separate approximately five-second delay can occur inside VS Code Core after `executeCommand` is called; that Core behavior is not changed here. ## Testing - `npm run compile-tests` - `npm run lint` - `npm run compile` - Unit suite via Mocha with Node 22: 1871 passing, 8 pending
1 parent 7b1e096 commit c8d26d9

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

‎src/features/terminal/runInTerminal.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { identifyTerminalShell } from '../common/shellDetector';
77
import { quoteArgs } from '../execution/execUtils';
88
import { normalizeShellPath } from './shells/common/shellUtils';
99
import { traceLog } from '../../common/logging';
10+
import { waitForShellIntegration } from './utils';
1011

1112
export async function runInTerminal(
1213
environment: PythonEnvironment,
@@ -26,6 +27,9 @@ export async function runInTerminal(
2627
if (shellType === ShellConstants.GITBASH) {
2728
executable = normalizeShellPath(executable, shellType);
2829
}
30+
if (!terminal.shellIntegration) {
31+
await waitForShellIntegration(terminal);
32+
}
2933
if (terminal.shellIntegration) {
3034
let execution: TerminalShellExecution | undefined;
3135
const deferred = createDeferred<void>();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import { Disposable, Terminal, TerminalShellExecution, TerminalShellExecutionEndEvent } from 'vscode';
4+
import * as windowApis from '../../../common/window.apis';
5+
import * as shellDetector from '../../../features/common/shellDetector';
6+
import { runInTerminal } from '../../../features/terminal/runInTerminal';
7+
import * as terminalUtils from '../../../features/terminal/utils';
8+
import { createMockPythonEnvironment } from '../../mocks/pythonEnvironment';
9+
10+
suite('runInTerminal', () => {
11+
teardown(() => {
12+
sinon.restore();
13+
});
14+
15+
test('waits for shell integration before executing a command', async () => {
16+
sinon.stub(shellDetector, 'identifyTerminalShell').returns('fish');
17+
const waitForShellIntegrationStub = sinon.stub(terminalUtils, 'waitForShellIntegration');
18+
let resolveWaitForShellIntegration!: (result: boolean) => void;
19+
const waitForShellIntegrationPromise = new Promise<boolean>((resolve) => {
20+
resolveWaitForShellIntegration = resolve;
21+
});
22+
23+
const execution = {} as TerminalShellExecution;
24+
const shellIntegration = {
25+
executeCommand: sinon.stub().returns(execution),
26+
};
27+
const terminal = {
28+
name: 'Python',
29+
shellIntegration: undefined,
30+
sendText: sinon.stub(),
31+
} as unknown as Terminal;
32+
waitForShellIntegrationStub.returns(waitForShellIntegrationPromise);
33+
34+
let endListener: ((event: TerminalShellExecutionEndEvent) => void) | undefined;
35+
sinon.stub(windowApis, 'onDidEndTerminalShellExecution').callsFake((listener) => {
36+
endListener = listener;
37+
return new Disposable(() => undefined);
38+
});
39+
40+
const environment = createMockPythonEnvironment({ envPath: '/env/bin/python' });
41+
const runPromise = runInTerminal(environment, terminal, { cwd: '/workspace', args: ['main.py'] });
42+
await new Promise<void>((resolve) => setImmediate(resolve));
43+
44+
sinon.assert.calledOnce(waitForShellIntegrationStub);
45+
sinon.assert.notCalled(terminal.sendText as sinon.SinonStub);
46+
sinon.assert.notCalled(shellIntegration.executeCommand);
47+
48+
(terminal as { shellIntegration?: typeof shellIntegration }).shellIntegration = shellIntegration;
49+
resolveWaitForShellIntegration(true);
50+
await new Promise<void>((resolve) => setImmediate(resolve));
51+
52+
sinon.assert.notCalled(terminal.sendText as sinon.SinonStub);
53+
sinon.assert.calledOnce(shellIntegration.executeCommand);
54+
assert.ok(endListener, 'shell execution end listener should be registered');
55+
56+
endListener!({ terminal, execution } as unknown as TerminalShellExecutionEndEvent);
57+
await runPromise;
58+
});
59+
60+
test('uses sendText when shell integration is unavailable after waiting', async () => {
61+
sinon.stub(shellDetector, 'identifyTerminalShell').returns('fish');
62+
const waitForShellIntegrationStub = sinon.stub(terminalUtils, 'waitForShellIntegration').resolves(false);
63+
const sendText = sinon.stub();
64+
const terminal = {
65+
name: 'Python',
66+
shellIntegration: undefined,
67+
sendText,
68+
} as unknown as Terminal;
69+
70+
const environment = createMockPythonEnvironment({ envPath: '/env/bin/python' });
71+
await runInTerminal(environment, terminal, { cwd: '/workspace', args: ['main.py'] });
72+
73+
sinon.assert.calledOnceWithExactly(waitForShellIntegrationStub, terminal);
74+
sinon.assert.calledOnceWithExactly(sendText, 'python main.py\n');
75+
});
76+
});

0 commit comments

Comments
 (0)