Skip to content

Commit edd4c31

Browse files
authored
Ensure to show terminal on run Python file with command activated terminal (#1129)
Resolves: #1128
1 parent d0edb90 commit edd4c31

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

src/features/terminal/terminalManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ export class TerminalManagerImpl implements TerminalManager {
306306
// We add it to skip activation on open to prevent double activation.
307307
// We can activate it ourselves since we are creating it.
308308
this.skipActivationOnOpen.add(newTerminal);
309+
310+
// Show terminal before activation so users can see the activation happening, requested script running.
311+
// Necessary for scenarios such as when terminal is awaiting user input, etc.
312+
newTerminal.show();
313+
309314
await this.autoActivateOnTerminalOpen(newTerminal, environment);
310315
}
311316

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import * as sinon from 'sinon';
6+
import { Disposable, Event, EventEmitter, Progress, Terminal, TerminalOptions, Uri } from 'vscode';
7+
import { PythonEnvironment } from '../../../api';
8+
import * as windowApis from '../../../common/window.apis';
9+
import * as workspaceApis from '../../../common/workspace.apis';
10+
import * as activationUtils from '../../../features/common/activation';
11+
import * as shellDetector from '../../../features/common/shellDetector';
12+
import {
13+
ShellEnvsProvider,
14+
ShellStartupScriptProvider,
15+
} from '../../../features/terminal/shells/startupProvider';
16+
import {
17+
DidChangeTerminalActivationStateEvent,
18+
TerminalActivationInternal,
19+
} from '../../../features/terminal/terminalActivationState';
20+
import { TerminalManagerImpl } from '../../../features/terminal/terminalManager';
21+
import * as terminalUtils from '../../../features/terminal/utils';
22+
23+
/**
24+
* Test implementation of TerminalActivationInternal that tracks method calls.
25+
*/
26+
class TestTerminalActivation implements TerminalActivationInternal {
27+
public callOrder: string[] = [];
28+
public activateCalls = 0;
29+
public deactivateCalls = 0;
30+
31+
private onDidChangeEmitter = new EventEmitter<DidChangeTerminalActivationStateEvent>();
32+
public onDidChangeTerminalActivationState: Event<DidChangeTerminalActivationStateEvent> =
33+
this.onDidChangeEmitter.event;
34+
35+
isActivated(_terminal: Terminal, _environment?: PythonEnvironment): boolean {
36+
return false;
37+
}
38+
39+
async activate(_terminal: Terminal, _environment: PythonEnvironment): Promise<void> {
40+
this.activateCalls += 1;
41+
this.callOrder.push('activate');
42+
}
43+
44+
async deactivate(_terminal: Terminal): Promise<void> {
45+
this.deactivateCalls += 1;
46+
}
47+
48+
getEnvironment(_terminal: Terminal): PythonEnvironment | undefined {
49+
return undefined;
50+
}
51+
52+
updateActivationState(_terminal: Terminal, _environment: PythonEnvironment, _activated: boolean): void {
53+
// Not used in these tests
54+
}
55+
56+
dispose(): void {
57+
this.onDidChangeEmitter.dispose();
58+
}
59+
}
60+
61+
suite('TerminalManager - create()', () => {
62+
let terminalActivation: TestTerminalActivation;
63+
let mockGetAutoActivationType: sinon.SinonStub;
64+
let terminalManager: TerminalManagerImpl;
65+
let mockTerminal: Partial<Terminal> & { show: sinon.SinonStub };
66+
67+
const createMockEnvironment = (): PythonEnvironment => ({
68+
envId: { id: 'test-env-id', managerId: 'test-manager' },
69+
name: 'Test Environment',
70+
displayName: 'Test Environment',
71+
shortDisplayName: 'TestEnv',
72+
displayPath: '/path/to/env',
73+
version: '3.9.0',
74+
environmentPath: Uri.file('/path/to/python'),
75+
sysPrefix: '/path/to/env',
76+
execInfo: {
77+
run: { executable: '/path/to/python' },
78+
activation: [{ executable: '/path/to/activate' }],
79+
},
80+
});
81+
82+
setup(() => {
83+
terminalActivation = new TestTerminalActivation();
84+
85+
mockTerminal = {
86+
name: 'Test Terminal',
87+
creationOptions: {} as TerminalOptions,
88+
shellIntegration: undefined,
89+
show: sinon.stub().callsFake(() => {
90+
terminalActivation.callOrder.push('show');
91+
}),
92+
sendText: sinon.stub(),
93+
};
94+
95+
mockGetAutoActivationType = sinon.stub(terminalUtils, 'getAutoActivationType');
96+
sinon.stub(terminalUtils, 'waitForShellIntegration').resolves(false);
97+
sinon.stub(activationUtils, 'isActivatableEnvironment').returns(true);
98+
sinon.stub(shellDetector, 'identifyTerminalShell').returns('bash');
99+
100+
sinon.stub(windowApis, 'createTerminal').returns(mockTerminal as Terminal);
101+
sinon.stub(windowApis, 'onDidOpenTerminal').returns(new Disposable(() => {}));
102+
sinon.stub(windowApis, 'onDidCloseTerminal').returns(new Disposable(() => {}));
103+
sinon.stub(windowApis, 'onDidChangeWindowState').returns(new Disposable(() => {}));
104+
sinon.stub(windowApis, 'terminals').returns([]);
105+
sinon.stub(windowApis, 'withProgress').callsFake(async (_options, task) => {
106+
const mockProgress: Progress<{ message?: string; increment?: number }> = { report: () => {} };
107+
const mockCancellationToken = {
108+
isCancellationRequested: false,
109+
onCancellationRequested: () => new Disposable(() => {}),
110+
};
111+
return task(mockProgress, mockCancellationToken as never);
112+
});
113+
114+
sinon.stub(workspaceApis, 'onDidChangeConfiguration').returns(new Disposable(() => {}));
115+
});
116+
117+
teardown(() => {
118+
sinon.restore();
119+
terminalActivation.dispose();
120+
});
121+
122+
function createTerminalManager(): TerminalManagerImpl {
123+
const emptyEnvProviders: ShellEnvsProvider[] = [];
124+
const emptyScriptProviders: ShellStartupScriptProvider[] = [];
125+
return new TerminalManagerImpl(terminalActivation, emptyEnvProviders, emptyScriptProviders);
126+
}
127+
128+
// Regression test for https://github.com/microsoft/vscode-python-environments/issues/640
129+
// With ACT_TYPE_COMMAND, create() awaits activation which blocks returning the terminal.
130+
// Without showing the terminal early, users wouldn't see it until activation completes (2-5 seconds).
131+
test('ACT_TYPE_COMMAND: shows terminal before awaiting activation to prevent hidden terminal during activation', async () => {
132+
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_COMMAND);
133+
terminalManager = createTerminalManager();
134+
const env = createMockEnvironment();
135+
136+
await terminalManager.create(env, { cwd: '/workspace' });
137+
138+
const { callOrder } = terminalActivation;
139+
assert.ok(callOrder.includes('show'), 'Terminal show() should be called');
140+
assert.ok(callOrder.includes('activate'), 'Terminal activate() should be called');
141+
const showIndex = callOrder.indexOf('show');
142+
const activateIndex = callOrder.indexOf('activate');
143+
assert.ok(
144+
showIndex < activateIndex,
145+
`show() at index ${showIndex} must precede activate() at index ${activateIndex}`,
146+
);
147+
});
148+
149+
// With ACT_TYPE_SHELL/OFF, create() returns immediately without blocking.
150+
// The caller (runInTerminal) handles showing the terminal, so create() shouldn't call show().
151+
test('ACT_TYPE_SHELL: does not call show() since create() returns immediately and caller handles visibility', async () => {
152+
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_SHELL);
153+
terminalManager = createTerminalManager();
154+
const env = createMockEnvironment();
155+
156+
await terminalManager.create(env, { cwd: '/workspace' });
157+
158+
const { callOrder } = terminalActivation;
159+
assert.strictEqual(callOrder.includes('show'), false, 'show() deferred to caller');
160+
assert.strictEqual(callOrder.includes('activate'), false, 'No command activation for shell startup mode');
161+
});
162+
163+
test('ACT_TYPE_OFF: does not call show() since create() returns immediately and caller handles visibility', async () => {
164+
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_OFF);
165+
terminalManager = createTerminalManager();
166+
const env = createMockEnvironment();
167+
168+
await terminalManager.create(env, { cwd: '/workspace' });
169+
170+
const { callOrder } = terminalActivation;
171+
assert.strictEqual(callOrder.includes('show'), false, 'show() deferred to caller');
172+
assert.strictEqual(callOrder.includes('activate'), false, 'Activation disabled');
173+
});
174+
175+
test('disableActivation option: skips both show() and activation, returns terminal immediately', async () => {
176+
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_COMMAND);
177+
terminalManager = createTerminalManager();
178+
const env = createMockEnvironment();
179+
180+
const terminal = await terminalManager.create(env, { cwd: '/workspace', disableActivation: true });
181+
182+
const { callOrder } = terminalActivation;
183+
assert.ok(terminal, 'Terminal should be returned');
184+
assert.strictEqual(callOrder.includes('show'), false, 'No show() when activation skipped');
185+
assert.strictEqual(terminalActivation.activateCalls, 0, 'No activate() when disableActivation is true');
186+
});
187+
});

0 commit comments

Comments
 (0)