diff --git a/src/features/terminal/terminalManager.ts b/src/features/terminal/terminalManager.ts index 5a6e9ff2..cb8c48fa 100644 --- a/src/features/terminal/terminalManager.ts +++ b/src/features/terminal/terminalManager.ts @@ -33,6 +33,7 @@ import { AutoActivationType, getAutoActivationType, getEnvironmentForTerminal, + shouldActivateInCurrentTerminal, waitForShellIntegration, } from './utils'; @@ -405,8 +406,21 @@ export class TerminalManagerImpl implements TerminalManager { public async initialize(api: PythonEnvironmentApi): Promise { const actType = getAutoActivationType(); + + // When activateEnvInCurrentTerminal is explicitly false, + // skip activation for ALL pre-existing terminals (terminals open before extension load). + // New terminals opened after extension load are still activated via autoActivateOnTerminalOpen. + const skipPreExistingTerminals = !shouldActivateInCurrentTerminal() && terminals().length > 0; + if (skipPreExistingTerminals) { + traceVerbose( + 'python.terminal.activateEnvInCurrentTerminal is explicitly disabled, skipping activation for pre-existing terminals', + ); + } + if (actType === ACT_TYPE_COMMAND) { - await Promise.all(terminals().map(async (t) => this.activateUsingCommand(api, t))); + if (!skipPreExistingTerminals) { + await Promise.all(terminals().map(async (t) => this.activateUsingCommand(api, t))); + } } else if (actType === ACT_TYPE_SHELL) { const shells = new Set( terminals() @@ -415,14 +429,16 @@ export class TerminalManagerImpl implements TerminalManager { ); if (shells.size > 0) { await this.handleSetupCheck(shells); - await Promise.all( - terminals().map(async (t) => { - // If the shell is not set up, we activate using command fallback. - if (this.shellSetup.get(identifyTerminalShell(t)) === false) { - await this.activateUsingCommand(api, t); - } - }), - ); + if (!skipPreExistingTerminals) { + await Promise.all( + terminals().map(async (t) => { + // If the shell is not set up, we activate using command fallback. + if (this.shellSetup.get(identifyTerminalShell(t)) === false) { + await this.activateUsingCommand(api, t); + } + }), + ); + } } } } diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts index d279a11e..54f1a2ca 100644 --- a/src/features/terminal/utils.ts +++ b/src/features/terminal/utils.ts @@ -262,6 +262,52 @@ export async function setAutoActivationType(value: AutoActivationType): Promise< return await config.update('terminal.autoActivationType', value, true); } +/** + * Determines whether activation commands should be sent to pre-existing terminals + * (terminals open before extension load). + * + * Checks the legacy `python.terminal.activateEnvInCurrentTerminal` setting using `inspect()` + * to distinguish between the default value and an explicitly user-set value. + * + * Priority: workspaceFolderValue > workspaceValue > globalRemoteValue > globalLocalValue > globalValue + * (matches the precedence used by getShellIntegrationEnabledCache and getAutoActivationType) + * + * - If the user has explicitly set the value to `false` at any scope, returns `false`. + * - Otherwise (default or explicitly `true`), returns `true`. + * + * @returns `false` only when the user has explicitly set the setting to `false`; `true` otherwise. + */ +export function shouldActivateInCurrentTerminal(): boolean { + const pythonConfig = getConfiguration('python'); + const inspected = pythonConfig.inspect('terminal.activateEnvInCurrentTerminal'); + + if (!inspected) { + return true; + } + + // Only respect `false` when the user has deliberately set it. + // Priority: workspaceFolder > workspace > globalRemote > globalLocal > global + const inspectValue = inspected as Record; + + if (inspected.workspaceFolderValue === false) { + return false; + } + if (inspected.workspaceValue === false) { + return false; + } + if ('globalRemoteValue' in inspected && inspectValue.globalRemoteValue === false) { + return false; + } + if ('globalLocalValue' in inspected && inspectValue.globalLocalValue === false) { + return false; + } + if (inspected.globalValue === false) { + return false; + } + + return true; +} + export async function getAllDistinctProjectEnvironments( api: PythonProjectGetterApi & PythonProjectEnvironmentApi, ): Promise { diff --git a/src/test/features/terminal/terminalManager.unit.test.ts b/src/test/features/terminal/terminalManager.unit.test.ts index 8d26547c..953fe17c 100644 --- a/src/test/features/terminal/terminalManager.unit.test.ts +++ b/src/test/features/terminal/terminalManager.unit.test.ts @@ -421,3 +421,129 @@ suite('TerminalManager - terminal naming', () => { } }); }); + +suite('TerminalManager - initialize() with activateEnvInCurrentTerminal', () => { + let terminalActivation: TestTerminalActivation; + let terminalManager: TerminalManagerImpl; + let mockGetAutoActivationType: sinon.SinonStub; + let mockShouldActivateInCurrentTerminal: sinon.SinonStub; + let mockTerminals: sinon.SinonStub; + let mockGetEnvironmentForTerminal: sinon.SinonStub; + + const createMockTerminal = (name: string): Terminal => + ({ + name, + creationOptions: {} as TerminalOptions, + shellIntegration: undefined, + show: sinon.stub(), + sendText: sinon.stub(), + }) as unknown as Terminal; + + const createMockEnvironment = (): PythonEnvironment => ({ + envId: { id: 'test-env-id', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + shortDisplayName: 'TestEnv', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/python'), + sysPrefix: '/path/to/env', + execInfo: { + run: { executable: '/path/to/python' }, + activation: [{ executable: '/path/to/activate' }], + }, + }); + + setup(() => { + terminalActivation = new TestTerminalActivation(); + + mockGetAutoActivationType = sinon.stub(terminalUtils, 'getAutoActivationType'); + mockShouldActivateInCurrentTerminal = sinon.stub(terminalUtils, 'shouldActivateInCurrentTerminal'); + mockGetEnvironmentForTerminal = sinon.stub(terminalUtils, 'getEnvironmentForTerminal'); + sinon.stub(terminalUtils, 'waitForShellIntegration').resolves(false); + sinon.stub(activationUtils, 'isActivatableEnvironment').returns(true); + sinon.stub(shellDetector, 'identifyTerminalShell').returns('bash'); + + sinon.stub(windowApis, 'createTerminal').callsFake(() => createMockTerminal('new')); + sinon.stub(windowApis, 'onDidOpenTerminal').returns(new Disposable(() => {})); + sinon.stub(windowApis, 'onDidCloseTerminal').returns(new Disposable(() => {})); + sinon.stub(windowApis, 'onDidChangeWindowState').returns(new Disposable(() => {})); + sinon.stub(windowApis, 'activeTerminal').returns(undefined); + + mockTerminals = sinon.stub(windowApis, 'terminals'); + + sinon.stub(windowApis, 'withProgress').callsFake(async (_options, task) => { + const mockProgress = { report: () => {} }; + const mockToken = { + isCancellationRequested: false, + onCancellationRequested: () => new Disposable(() => {}), + }; + return task(mockProgress as never, mockToken as never); + }); + sinon.stub(workspaceApis, 'onDidChangeConfiguration').returns(new Disposable(() => {})); + }); + + teardown(() => { + sinon.restore(); + terminalActivation.dispose(); + }); + + function createTerminalManager(): TerminalManagerImpl { + return new TerminalManagerImpl(terminalActivation, [], []); + } + + test('initialize activates all pre-existing terminals when shouldActivateInCurrentTerminal returns true', async () => { + const terminal1 = createMockTerminal('terminal1'); + const terminal2 = createMockTerminal('terminal2'); + const env = createMockEnvironment(); + + mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_COMMAND); + mockShouldActivateInCurrentTerminal.returns(true); + mockTerminals.returns([terminal1, terminal2]); + mockGetEnvironmentForTerminal.resolves(env); + + terminalManager = createTerminalManager(); + await terminalManager.initialize({} as never); + + assert.strictEqual( + terminalActivation.activateCalls, + 2, + 'Should activate all pre-existing terminals when activateEnvInCurrentTerminal is true/default', + ); + }); + + test('initialize skips all pre-existing terminals when shouldActivateInCurrentTerminal returns false', async () => { + const terminal1 = createMockTerminal('terminal1'); + const terminal2 = createMockTerminal('terminal2'); + const env = createMockEnvironment(); + + mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_COMMAND); + mockShouldActivateInCurrentTerminal.returns(false); + mockTerminals.returns([terminal1, terminal2]); + mockGetEnvironmentForTerminal.resolves(env); + + terminalManager = createTerminalManager(); + await terminalManager.initialize({} as never); + + assert.strictEqual( + terminalActivation.activateCalls, + 0, + 'Should skip all pre-existing terminals when activateEnvInCurrentTerminal is explicitly false', + ); + }); + + test('initialize proceeds normally when shouldActivateInCurrentTerminal returns false but no pre-existing terminals', async () => { + mockGetAutoActivationType.returns(terminalUtils.ACT_TYPE_COMMAND); + mockShouldActivateInCurrentTerminal.returns(false); + mockTerminals.returns([]); + + terminalManager = createTerminalManager(); + await terminalManager.initialize({} as never); + + assert.strictEqual( + terminalActivation.activateCalls, + 0, + 'Should have no activations when there are no terminals', + ); + }); +}); diff --git a/src/test/features/terminal/utils.unit.test.ts b/src/test/features/terminal/utils.unit.test.ts index d97030c0..e65f9659 100644 --- a/src/test/features/terminal/utils.unit.test.ts +++ b/src/test/features/terminal/utils.unit.test.ts @@ -7,6 +7,7 @@ import { ACT_TYPE_SHELL, AutoActivationType, getAutoActivationType, + shouldActivateInCurrentTerminal, } from '../../../features/terminal/utils'; interface MockWorkspaceConfig { @@ -354,3 +355,177 @@ suite('Terminal Utils - getAutoActivationType', () => { }); }); }); + +suite('Terminal Utils - shouldActivateInCurrentTerminal', () => { + let mockGetConfiguration: sinon.SinonStub; + let pythonConfig: MockWorkspaceConfig; + + setup(() => { + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + mockGetConfiguration.withArgs('python').returns(pythonConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('should return true when inspect returns undefined (no config)', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns(undefined); + + assert.strictEqual(shouldActivateInCurrentTerminal(), true, 'Should default to true when no config exists'); + }); + + test('should return true when no explicit values are set (all undefined)', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + true, + 'Should return true when only defaultValue is set (not user-explicit)', + ); + }); + + test('should return false when globalValue is explicitly false', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalValue: false, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + false, + 'Should return false when user explicitly set globalValue to false', + ); + }); + + test('should return false when workspaceValue is explicitly false', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalValue: undefined, + workspaceValue: false, + workspaceFolderValue: undefined, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + false, + 'Should return false when user explicitly set workspaceValue to false', + ); + }); + + test('should return false when workspaceFolderValue is explicitly false', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: false, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + false, + 'Should return false when user explicitly set workspaceFolderValue to false', + ); + }); + + test('should return true when globalValue is explicitly true', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalValue: true, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + true, + 'Should return true when user explicitly set globalValue to true', + ); + }); + + test('workspaceFolderValue false takes precedence over globalValue true', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalValue: true, + workspaceValue: undefined, + workspaceFolderValue: false, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + false, + 'workspaceFolderValue false should take precedence', + ); + }); + + test('should return false when globalRemoteValue is explicitly false', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalRemoteValue: false, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + false, + 'Should return false when user explicitly set globalRemoteValue to false', + ); + }); + + test('should return false when globalLocalValue is explicitly false', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalLocalValue: false, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + false, + 'Should return false when user explicitly set globalLocalValue to false', + ); + }); + + test('workspaceValue false takes precedence over globalRemoteValue true', () => { + pythonConfig.inspect.withArgs('terminal.activateEnvInCurrentTerminal').returns({ + key: 'terminal.activateEnvInCurrentTerminal', + defaultValue: false, + globalRemoteValue: true, + globalValue: undefined, + workspaceValue: false, + workspaceFolderValue: undefined, + }); + + assert.strictEqual( + shouldActivateInCurrentTerminal(), + false, + 'workspaceValue false should take precedence over globalRemoteValue true', + ); + }); +});