Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 10 additions & 9 deletions src/features/envCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,16 @@ export async function createTerminalCommand(
tm: TerminalManager,
): Promise<Terminal | undefined> {
const pythonProjects = api.getPythonProjects();
if (context instanceof PythonEnvTreeItem) {
const pw = await pickProject(pythonProjects);
if (pythonProjects.length > 0 && !pw) {
return undefined;
}
const cwd = pw ? await findParentIfFile(pw.uri.fsPath) : undefined;
const terminal = await tm.create(context.environment, { cwd });
terminal.show();
return terminal;
}
// If no context is provided, or there are multiple projects, prompt the user to select a project for the terminal's cwd
if (context === undefined || pythonProjects.length > 0) {
const pw = await pickProject(pythonProjects);
Expand Down Expand Up @@ -848,15 +858,6 @@ export async function createTerminalCommand(
terminal.show();
return terminal;
}
} else if (context instanceof PythonEnvTreeItem) {
const view = context as PythonEnvTreeItem;
const pw = await pickProject(pythonProjects);
if (pw) {
const cwd = await findParentIfFile(pw.uri.fsPath);
const terminal = await tm.create(view.environment, { cwd });
terminal.show();
return terminal;
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/features/terminal/terminalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class TerminalManagerImpl implements TerminalManager {
private disposables: Disposable[] = [];
private skipActivationOnOpen = new Set<Terminal>();
private shellSetup: Map<string, boolean> = new Map<string, boolean>();
private pendingTerminalEnvironments = new WeakMap<Terminal, PythonEnvironment>();

private onTerminalOpenedEmitter = new EventEmitter<Terminal>();
private onTerminalOpened = this.onTerminalOpenedEmitter.event;
Expand Down Expand Up @@ -99,7 +100,8 @@ export class TerminalManagerImpl implements TerminalManager {
if (this.skipActivationOnOpen.has(t) || shouldSkipTerminalActivation(t)) {
return;
}
let env = this.ta.getEnvironment(t);
let env = this.pendingTerminalEnvironments.get(t) ?? this.ta.getEnvironment(t);
this.pendingTerminalEnvironments.delete(t);
if (!env) {
const api = await getPythonApi();
env = await getEnvironmentForTerminal(api, t);
Expand Down Expand Up @@ -296,6 +298,9 @@ export class TerminalManagerImpl implements TerminalManager {
name,
env: envVars,
});
if (autoActType === ACT_TYPE_SHELL) {
this.pendingTerminalEnvironments.set(newTerminal, environment);
}

if (autoActType === ACT_TYPE_COMMAND) {
if (options.disableActivation) {
Expand Down
106 changes: 105 additions & 1 deletion src/test/features/envCommands.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
clearEnvironmentCachesCommand,
clearScriptEnvironmentCacheCommand,
createAnyEnvironmentCommand,
createTerminalCommand,
removeEnvironmentCommand,
removePythonProject,
revealEnvInManagerView,
Expand All @@ -26,7 +27,12 @@ import * as shellProviders from '../../features/terminal/shells/providers';
import { ShellStartupScriptProvider } from '../../features/terminal/shells/startupProvider';
import { TerminalManager } from '../../features/terminal/terminalManager';
import { EnvManagerView } from '../../features/views/envManagersView';
import { ProjectEnvironment, ProjectItem } from '../../features/views/treeViewItems';
import {
EnvManagerTreeItem,
ProjectEnvironment,
ProjectItem,
PythonEnvTreeItem,
} from '../../features/views/treeViewItems';
import type { EnvironmentManagers } from '../../features/envManagers';
import type { PythonProjectManager } from '../../features/projectManager';
import { InternalEnvironmentManager } from '../../managers/common/registeredManagers';
Expand Down Expand Up @@ -94,6 +100,104 @@ suite('Environment removal command ownership', () => {
});
});

suite('Create Terminal Command Tests', () => {
teardown(() => {
sinon.restore();
});

function createEnvironmentItem(environment: PythonEnvironment): PythonEnvTreeItem {
const manager = {
id: environment.envId.managerId,
name: 'venv',
displayName: 'Venv',
} as InternalEnvironmentManager;
return new PythonEnvTreeItem(environment, new EnvManagerTreeItem(manager));
}

function createTerminalManager() {
const terminal = { show: sinon.stub() } as unknown as Terminal;
const create = sinon.stub().resolves(terminal);
return { terminal, create, manager: { create } as unknown as TerminalManager };
}

test('creates a terminal for the clicked environment without a project', async () => {
const environment = createMockPythonEnvironment({
managerId: 'ms-python.python:venv',
envPath: path.join(process.cwd(), '.venv', 'python'),
});
const getEnvironment = sinon.stub().resolves(undefined);
const api = {
getPythonProjects: sinon.stub().returns([]),
getEnvironment,
} as unknown as PythonEnvironmentApi;
const terminalManager = createTerminalManager();

const result = await createTerminalCommand(createEnvironmentItem(environment), api, terminalManager.manager);

assert.strictEqual(result, terminalManager.terminal);
sinon.assert.calledOnceWithExactly(terminalManager.create, environment, { cwd: undefined });
sinon.assert.calledOnce(terminalManager.terminal.show as sinon.SinonStub);
sinon.assert.notCalled(getEnvironment);
});

test('does not create a terminal when project selection is cancelled', async () => {
const environment = createMockPythonEnvironment({
managerId: 'ms-python.python:venv',
envPath: path.join(process.cwd(), '.venv', 'python'),
});
const projects: PythonProject[] = [
{ name: 'project-one', uri: Uri.file(path.join(process.cwd(), 'project-one')) },
{ name: 'project-two', uri: Uri.file(path.join(process.cwd(), 'project-two')) },
];
const api = {
getPythonProjects: sinon.stub().returns(projects),
} as unknown as PythonEnvironmentApi;
const terminalManager = createTerminalManager();
sinon.stub(projectApi, 'pickProject').resolves(undefined);

const result = await createTerminalCommand(createEnvironmentItem(environment), api, terminalManager.manager);

assert.strictEqual(result, undefined);
sinon.assert.notCalled(terminalManager.create);
});

for (const projectEnvironment of [
undefined,
createMockPythonEnvironment({
name: 'project-environment',
managerId: 'ms-python.python:venv',
envPath: path.join(process.cwd(), 'project-environment', 'python'),
}),
]) {
const projectEnvironmentDescription = projectEnvironment ? 'a different environment' : 'no environment';

test(`creates a terminal for the clicked environment when the project has ${projectEnvironmentDescription}`, async () => {
const environment = createMockPythonEnvironment({
name: 'clicked-environment',
managerId: 'ms-python.python:venv',
envPath: path.join(process.cwd(), 'clicked-environment', 'python'),
});
const project: PythonProject = {
name: 'project',
uri: Uri.file(process.cwd()),
};
const getEnvironment = sinon.stub().resolves(projectEnvironment);
const api = {
getPythonProjects: sinon.stub().returns([project]),
getEnvironment,
} as unknown as PythonEnvironmentApi;
const terminalManager = createTerminalManager();

const result = await createTerminalCommand(createEnvironmentItem(environment), api, terminalManager.manager);

assert.strictEqual(result, terminalManager.terminal);
sinon.assert.calledOnceWithExactly(terminalManager.create, environment, { cwd: project.uri.fsPath });
sinon.assert.calledOnce(terminalManager.terminal.show as sinon.SinonStub);
sinon.assert.notCalled(getEnvironment);
});
}
});

suite('Create Any Environment Command Tests', () => {
let em: typeMoq.IMock<EnvironmentManagers>;
let pm: typeMoq.IMock<PythonProjectManager>;
Expand Down
35 changes: 32 additions & 3 deletions src/test/features/terminal/terminalManager.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import {
Uri,
WorkspaceConfiguration,
} from 'vscode';
import { PythonEnvironment } from '../../../api';
import { PythonEnvironment, PythonEnvironmentApi } from '../../../api';
import * as windowApis from '../../../common/window.apis';
import * as workspaceApis from '../../../common/workspace.apis';
import * as extensionApi from '../../../extensionApi';
import * as activationUtils from '../../../features/common/activation';
import * as shellDetector from '../../../features/common/shellDetector';
import * as shellUtils from '../../../features/terminal/shells/common/shellUtils';
Expand All @@ -44,6 +45,7 @@ class TestTerminalActivation implements TerminalActivationInternal {
public callOrder: string[] = [];
public activateCalls = 0;
public deactivateCalls = 0;
public activatedEnvironment: PythonEnvironment | undefined;

private onDidChangeEmitter = new EventEmitter<DidChangeTerminalActivationStateEvent>();
public onDidChangeTerminalActivationState: Event<DidChangeTerminalActivationStateEvent> =
Expand All @@ -53,8 +55,9 @@ class TestTerminalActivation implements TerminalActivationInternal {
return false;
}

async activate(_terminal: Terminal, _environment: PythonEnvironment): Promise<void> {
async activate(_terminal: Terminal, environment: PythonEnvironment): Promise<void> {
this.activateCalls += 1;
this.activatedEnvironment = environment;
this.callOrder.push('activate');
}

Expand All @@ -80,6 +83,7 @@ suite('TerminalManager - create()', () => {
let mockGetAutoActivationType: sinon.SinonStub;
let terminalManager: TerminalManagerImpl;
let mockTerminal: Partial<Terminal> & { show: sinon.SinonStub };
let openTerminal: (terminal: Terminal) => unknown;

const createMockEnvironment = (): PythonEnvironment => ({
envId: { id: 'test-env-id', managerId: 'test-manager' },
Expand Down Expand Up @@ -115,7 +119,10 @@ suite('TerminalManager - create()', () => {
sinon.stub(shellDetector, 'identifyTerminalShell').returns('bash');

sinon.stub(windowApis, 'createTerminal').returns(mockTerminal as Terminal);
sinon.stub(windowApis, 'onDidOpenTerminal').returns(new Disposable(() => {}));
sinon.stub(windowApis, 'onDidOpenTerminal').callsFake((listener) => {
openTerminal = listener;
return new Disposable(() => {});
});
sinon.stub(windowApis, 'onDidCloseTerminal').returns(new Disposable(() => {}));
sinon.stub(windowApis, 'onDidChangeWindowState').returns(new Disposable(() => {}));
sinon.stub(windowApis, 'terminals').returns([]);
Expand Down Expand Up @@ -177,6 +184,28 @@ suite('TerminalManager - create()', () => {
assert.strictEqual(callOrder.includes('activate'), false, 'No command activation for shell startup mode');
});

test('ACT_TYPE_SHELL: command fallback activates the environment requested during creation', async () => {
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_SHELL);
terminalManager = createTerminalManager();
const requestedEnvironment = createMockEnvironment();
const projectEnvironment = {
...createMockEnvironment(),
envId: { id: 'project-env-id', managerId: 'test-manager' },
};
const cwd = Uri.file(process.cwd()).fsPath;
Object.defineProperty(mockTerminal, 'creationOptions', { value: { cwd } });
sinon.stub(extensionApi, 'getPythonApi').resolves({
getPythonProjects: () => [{ name: 'Project', uri: Uri.file(cwd) }],
getEnvironment: async () => projectEnvironment,
} as unknown as PythonEnvironmentApi);

const terminal = await terminalManager.create(requestedEnvironment, { cwd });
openTerminal(terminal);
await new Promise((resolve) => setImmediate(resolve));

assert.strictEqual(terminalActivation.activatedEnvironment, requestedEnvironment);
});

test('ACT_TYPE_OFF: does not call show() since create() returns immediately and caller handles visibility', async () => {
mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_OFF);
terminalManager = createTerminalManager();
Expand Down