From 5f5ef21ec46a8573c5d444b4cd9424b1b859bb7f Mon Sep 17 00:00:00 2001 From: Stella Huang Date: Thu, 12 Nov 2026 13:20:53 -0800 Subject: [PATCH] Align Python UI visibility with extension activation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- package.json | 26 +++--- src/common/constants.ts | 1 + src/extension.ts | 4 + src/features/settings/settingHelpers.ts | 15 ++++ src/test/contributionVisibility.unit.test.ts | 82 +++++++++++++++++++ .../settingHelpers.activation.unit.test.ts | 49 ++++++++++- 6 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 src/test/contributionVisibility.unit.test.ts diff --git a/package.json b/package.json index 91a1b3364..f2650b3e8 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "title": "%python-envs.setEnvManager.title%", "category": "Python", "icon": "$(gear)", - "when": "config.python.useEnvironmentsExtension != false" + "enablement": "pythonEnvs.enabled" }, { "command": "python-envs.setPkgManager", @@ -403,7 +403,7 @@ "title": "%python-envs.runPetInTerminal.title%", "category": "Python", "icon": "$(terminal)", - "when": "config.python.useEnvironmentsExtension != false" + "enablement": "pythonEnvs.enabled" } ], "menus": { @@ -442,19 +442,19 @@ }, { "command": "python-envs.setEnvManager", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" }, { "command": "python-envs.packages", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" }, { "command": "python-envs.set", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" }, { "command": "python-envs.setPkgManager", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" }, { "command": "python-envs.removePythonProject", @@ -470,7 +470,7 @@ }, { "command": "python-envs.runAsTask", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" }, { "command": "python-envs.clearScriptEnvCache", @@ -522,15 +522,15 @@ }, { "command": "python-envs.createNewProjectFromTemplate", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" }, { "command": "python-envs.terminal.revertStartupScriptChanges", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" }, { "command": "python-envs.reportIssue", - "when": "config.python.useEnvironmentsExtension != false" + "when": "pythonEnvs.enabled" } ], "view/item/context": [ @@ -734,7 +734,7 @@ "id": "python", "title": "Python", "icon": "files/logo.svg", - "when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython" + "when": "pythonEnvs.enabled && python-envs.workspaceHasPython" } ] }, @@ -745,14 +745,14 @@ "name": "Python Projects", "icon": "files/logo.svg", "contextualTitle": "Python Projects", - "when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython" + "when": "pythonEnvs.enabled && python-envs.workspaceHasPython" }, { "id": "env-managers", "name": "Environment Managers", "icon": "files/logo.svg", "contextualTitle": "Environment Managers", - "when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython" + "when": "pythonEnvs.enabled && python-envs.workspaceHasPython" } ] }, diff --git a/src/common/constants.ts b/src/common/constants.ts index 086e93766..4f0cad744 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -1,6 +1,7 @@ import * as path from 'path'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; +export const PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY = 'pythonEnvs.enabled'; export const PYTHON_EXTENSION_ID = 'ms-python.python'; export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; diff --git a/src/extension.ts b/src/extension.ts index e088619c2..07a3a67ae 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -86,6 +86,7 @@ import { registerCompletionProvider } from './features/settings/settingCompletio import { isEnvironmentsExtensionEnabled, migrateGlobalDefaultEnvManagerSetting, + setEnvironmentsExtensionEnabledContext, } from './features/settings/settingHelpers'; import { setActivateMenuButtonContext } from './features/terminal/activateMenuButton'; import { normalizeShellPath } from './features/terminal/shells/common/shellUtils'; @@ -145,6 +146,8 @@ export async function activate(context: ExtensionContext): Promise { } export async function deactivate(context: ExtensionContext) { + await setEnvironmentsExtensionEnabledContext(false); await disposeAll(context.subscriptions); if (context.subscriptions?.length) { context.subscriptions.length = 0; // Clear subscriptions to prevent memory leaks diff --git a/src/features/settings/settingHelpers.ts b/src/features/settings/settingHelpers.ts index 7c22b38df..d0b136f26 100644 --- a/src/features/settings/settingHelpers.ts +++ b/src/features/settings/settingHelpers.ts @@ -1,10 +1,12 @@ import * as path from 'path'; import { ConfigurationScope, ConfigurationTarget, Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; import { PythonProject } from '../../api'; +import { executeCommand } from '../../common/command.api'; import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID, INLINE_SCRIPT_MANAGER_ID, + PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY, SYSTEM_MANAGER_ID, } from '../../common/constants'; import { traceError, traceInfo, traceVerbose, traceWarn } from '../../common/logging'; @@ -1012,6 +1014,19 @@ export function isEnvironmentsExtensionEnabled(): boolean { ); } +/** + * Publishes whether the Python Environments extension is enabled for contribution visibility. + * + * @param enabled - The activation decision to expose through the VS Code context key. + */ +export async function setEnvironmentsExtensionEnabledContext(enabled: boolean): Promise { + try { + await executeCommand('setContext', PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY, enabled); + } catch (err) { + traceError(`Failed to set the Python Environments enabled context to ${enabled}`, err); + } +} + /** * Gets user-configured setting for user-scoped settings. * Only checks globalValue (ignores defaultValue). diff --git a/src/test/contributionVisibility.unit.test.ts b/src/test/contributionVisibility.unit.test.ts new file mode 100644 index 000000000..d63aad7ab --- /dev/null +++ b/src/test/contributionVisibility.unit.test.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY } from '../common/constants'; + +type ConditionalContribution = { + command?: string; + enablement?: string; + id?: string; + when?: string; +}; + +type PackageJson = { + contributes: { + commands: ConditionalContribution[]; + menus: { + commandPalette: ConditionalContribution[]; + }; + viewsContainers: { + activitybar: ConditionalContribution[]; + }; + views: { + python: ConditionalContribution[]; + }; + }; +}; + +const packageJson = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), 'utf8'), +) as PackageJson; + +suite('Contribution visibility', () => { + test('gates environment commands on the activation-owned context key', () => { + const commandIds = ['python-envs.setEnvManager', 'python-envs.runPetInTerminal']; + const commandPaletteIds = [ + 'python-envs.setEnvManager', + 'python-envs.packages', + 'python-envs.set', + 'python-envs.setPkgManager', + 'python-envs.runAsTask', + 'python-envs.createNewProjectFromTemplate', + 'python-envs.terminal.revertStartupScriptChanges', + 'python-envs.reportIssue', + ]; + + for (const commandId of commandIds) { + const contribution = packageJson.contributes.commands.find(({ command }) => command === commandId); + assert.ok(contribution, `Missing command contribution for ${commandId}`); + assert.strictEqual(contribution.enablement, PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY); + } + + for (const commandId of commandPaletteIds) { + const contribution = packageJson.contributes.menus.commandPalette.find( + ({ command }) => command === commandId, + ); + assert.ok(contribution, `Missing Command Palette contribution for ${commandId}`); + assert.strictEqual(contribution.when, PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY); + } + + assert.ok( + !JSON.stringify(packageJson.contributes).includes('config.python.useEnvironmentsExtension'), + 'Contribution visibility must not evaluate the raw setting independently of activation', + ); + }); + + test('gates the Python views on activation and Python workspace contexts', () => { + const when = `${PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY} && python-envs.workspaceHasPython`; + const activityBar = packageJson.contributes.viewsContainers.activitybar.find(({ id }) => id === 'python'); + const pythonProjects = packageJson.contributes.views.python.find(({ id }) => id === 'python-projects'); + const environmentManagers = packageJson.contributes.views.python.find(({ id }) => id === 'env-managers'); + + assert.ok(activityBar, 'Missing Python activity bar contribution'); + assert.ok(pythonProjects, 'Missing Python Projects view contribution'); + assert.ok(environmentManagers, 'Missing Environment Managers view contribution'); + assert.strictEqual(activityBar.when, when); + assert.strictEqual(pythonProjects.when, when); + assert.strictEqual(environmentManagers.when, when); + }); +}); diff --git a/src/test/features/settings/settingHelpers.activation.unit.test.ts b/src/test/features/settings/settingHelpers.activation.unit.test.ts index 8060fc794..369aec364 100644 --- a/src/test/features/settings/settingHelpers.activation.unit.test.ts +++ b/src/test/features/settings/settingHelpers.activation.unit.test.ts @@ -4,8 +4,14 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import * as commandApis from '../../../common/command.api'; +import { PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY } from '../../../common/constants'; +import * as logging from '../../../common/logging'; import * as workspaceApis from '../../../common/workspace.apis'; -import { isEnvironmentsExtensionEnabled } from '../../../features/settings/settingHelpers'; +import { + isEnvironmentsExtensionEnabled, + setEnvironmentsExtensionEnabledContext, +} from '../../../features/settings/settingHelpers'; type SettingInspection = { defaultValue?: boolean; @@ -33,6 +39,16 @@ suite('isEnvironmentsExtensionEnabled', () => { sinon.restore(); }); + test('enables when the setting is not registered', () => { + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(undefined); + sinon + .stub(workspaceApis, 'getConfiguration') + .withArgs('python', undefined) + .returns(createConfiguration(undefined)); + + assert.strictEqual(isEnvironmentsExtensionEnabled(), true); + }); + test('ignores a contributed default when no value is explicitly configured', () => { sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(undefined); sinon @@ -100,4 +116,35 @@ suite('isEnvironmentsExtensionEnabled', () => { assert.strictEqual(isEnvironmentsExtensionEnabled(), true); }); + + test('publishes enabled and disabled decisions to the contribution context', async () => { + const executeCommand = sinon.stub(commandApis, 'executeCommand').resolves(undefined); + + await setEnvironmentsExtensionEnabledContext(true); + await setEnvironmentsExtensionEnabledContext(false); + + assert.deepStrictEqual(executeCommand.firstCall.args, [ + 'setContext', + PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY, + true, + ]); + assert.deepStrictEqual(executeCommand.secondCall.args, [ + 'setContext', + PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY, + false, + ]); + }); + + test('logs context publication failures without rejecting activation', async () => { + const error = new Error('setContext failed'); + sinon.stub(commandApis, 'executeCommand').rejects(error); + const traceError = sinon.stub(logging, 'traceError'); + + await setEnvironmentsExtensionEnabledContext(true); + + assert.deepStrictEqual(traceError.firstCall.args, [ + 'Failed to set the Python Environments enabled context to true', + error, + ]); + }); });