Skip to content
Open
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
18 changes: 13 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@
}
},
"activationEvents": [
"onLanguage:python"
"onLanguage:python",
"workspaceContains:**/*.py",
"workspaceContains:**/*.pyi",
"workspaceContains:**/*.ipynb",
"workspaceContains:**/pyproject.toml",
"workspaceContains:**/requirements.txt",
"workspaceContains:**/Pipfile",
"workspaceContains:**/pyvenv.cfg",
"workspaceContains:mspythonconfig.json"
],
"homepage": "https://github.com/microsoft/vscode-python-environments",
"repository": {
Expand Down Expand Up @@ -688,7 +696,7 @@
"id": "python",
"title": "Python",
"icon": "files/logo.svg",
"when": "config.python.useEnvironmentsExtension != false"
"when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython"
}
]
},
Expand All @@ -699,14 +707,14 @@
"name": "Python Projects",
"icon": "files/logo.svg",
"contextualTitle": "Python Projects",
"when": "config.python.useEnvironmentsExtension != false"
"when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR hides the activity-bar icon unless this context key is true:

python-envs.workspaceHasPython

That key is not defined initially. It only gets set after the extension’s  activate()  function runs:

registerWorkspacePythonContext(context.subscriptions);

However,  package.json currently activates the extension only when VS Code opens a Python-language document:

"activationEvents": [ "onLanguage:python" ]

This creates a circular dependency:

  1. The extension must activate to detect Python files and set the context key.
  2. The activity-bar icon is hidden until that key is set.
  3. A hidden view cannot be opened to activate the extension.
  4. If no  .py  file is opened,  onLanguage:python never activates the extension.

Example

A user opens a repository containing:

my-project/
├── pyproject.toml
├── requirements.txt
└── README.md

The repository is clearly a Python project, but the user has not opened a  .py  file yet.

Expected: The Python activity-bar icon appears because pyproject.toml  identifies the workspace as Python.

Actual: The extension does not activate, so it never searches for  pyproject.toml . The context key remains unset, and the icon stays hidden.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fix is to add workspaceContains activation events to package.json so the extension activates even before a .py file is opened:

"activationEvents": [
"onLanguage:python",
"workspaceContains:**/*.py",
"workspaceContains:pyproject.toml",
"workspaceContains:requirements.txt",
"workspaceContains:Pipfile",
"workspaceContains:setup.py",
"workspaceContains:mspythonconfig.json",
"workspaceContains:.venv",
"workspaceContains:.conda"
]

This breaks the circular dependency - extension activates when any marker file is present in the workspace, sets the context key, and the icon appears without needing a .py file open first.

},
{
"id": "env-managers",
"name": "Environment Managers",
"icon": "files/logo.svg",
"contextualTitle": "Environment Managers",
"when": "config.python.useEnvironmentsExtension != false"
"when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython"
}
]
},
Expand Down Expand Up @@ -770,4 +778,4 @@
"overrides": {
"serialize-javascript": "^7.0.3"
}
}
}
13 changes: 9 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ import {
onDidChangeTerminalShellIntegration,
withProgress,
} from './common/window.apis';
import { getPythonApi, setPythonApi } from './extensionApi';
import { createManagerReady } from './features/common/managerReady';
import { AutoFindProjects } from './features/creators/autoFindProjects';
import { ExistingProjects } from './features/creators/existingProjects';
import { NewPackageProject } from './features/creators/newPackageProject';
import { NewScriptProject } from './features/creators/newScriptProject';
import type { ProjectCreators } from './features/creators/projectCreators';
import { ProjectCreatorsImpl } from './features/creators/projectCreators';
import {
addPythonProjectCommand,
Expand All @@ -65,6 +67,7 @@ import {
setEnvManagerCommand,
setPackageManagerCommand,
} from './features/envCommands';
import type { EnvironmentManagers } from './features/envManagers';
import { PythonEnvironmentManagers } from './features/envManagers';
import { EnvVarManager, PythonEnvVariableManager } from './features/execution/envVariableManager';
import { latchInlineScriptFeatureActivation } from './features/inlineScript/activation';
Expand All @@ -75,9 +78,9 @@ import {
applyInitialEnvironmentSelection,
registerInterpreterSettingsChangeListener,
} from './features/interpreterSelection';
import type { PythonProjectManager } from './features/projectManager';
import { PythonProjectManagerImpl } from './features/projectManager';
import { reportIssue } from './features/reportIssue';
import { getPythonApi, setPythonApi } from './extensionApi';
import { registerCompletionProvider } from './features/settings/settingCompletions';
import {
isEnvironmentsExtensionEnabled,
Expand All @@ -99,14 +102,12 @@ import { PythonStatusBarImpl } from './features/views/pythonStatusBar';
import { updateViewsAndStatus } from './features/views/revealHandler';
import { TemporaryStateManager } from './features/views/temporaryStateManager';
import { PythonEnvTreeItem } from './features/views/treeViewItems';
import { registerWorkspacePythonContext } from './features/views/workspacePythonContext';
import {
getEnvManagerAndPackageManagerConfigLevels,
isInlineScriptsFeatureEnabled,
runPetInTerminalImpl,
} from './helpers';
import type { ProjectCreators } from './features/creators/projectCreators';
import type { EnvironmentManagers } from './features/envManagers';
import type { PythonProjectManager } from './features/projectManager';
import { registerInlineScriptFeatures } from './managers/builtin/inlineScript/main';
import { registerSystemPythonFeatures } from './managers/builtin/main';
import { SysPythonManager } from './managers/builtin/sysPythonManager';
Expand Down Expand Up @@ -141,6 +142,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
const outputChannel: LogOutputChannel = createLogOutputChannel('Python Environments');
context.subscriptions.push(outputChannel, registerLogger(outputChannel));

// Track whether this is a Python workspace (drives the activity bar icon). Registered after the
// `useEnvironmentsExtension` guard and the logger so failures are reported.
registerWorkspacePythonContext(context.subscriptions);

ensureCorrectVersion();

// Log extension version for diagnostics
Expand Down
79 changes: 79 additions & 0 deletions src/features/views/workspacePythonContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Disposable, Uri } from 'vscode';
import { executeCommand } from '../../common/command.api';
import { traceError } from '../../common/logging';
import {
createFileSystemWatcher,
findFiles,
getOpenTextDocuments,
onDidChangeWorkspaceFolders,
onDidOpenTextDocument,
} from '../../common/workspace.apis';

export const PYTHON_WORKSPACE_KEY = 'python-envs.workspaceHasPython';

/**
* Files that identify a workspace as a Python one.
*
* `findFiles` only returns files, never directories, so virtual environments are detected through
* their `pyvenv.cfg` rather than a `.venv`/`.conda` folder. `*.py` already covers `setup.py`,
* `manage.py`, `app.py`, etc.
*
* Keep the `workspaceContains:` entries in `activationEvents` (package.json) in sync with this list,
* otherwise the extension is not activated and the context key is never set.
*/
export const MARKER_GLOB =
'**/{*.py,*.pyi,*.ipynb,pyproject.toml,requirements.txt,Pipfile,pyvenv.cfg,mspythonconfig.json}';
const EXCLUDE = '**/{node_modules,.git,site-packages}/**';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

The classifier omits legitimate notebook-only and stub-only workspaces (*.ipynb, *.pyi), while .venv and .conda may be directories that findFiles does not return. Reuse a canonical project classifier if available, or expand and test the taxonomy with reliable environment markers such as pyvenv.cfg.

// `createFileSystemWatcher` has no exclude option, so the same folders are filtered in the handlers.
const EXCLUDE_RE = /[\\/](node_modules|\.git|site-packages)[\\/]/;

async function hasPythonContent(): Promise<boolean> {
const markers = await findFiles(MARKER_GLOB, EXCLUDE, 1);
if (markers.length > 0) {
return true;
}
// `findFiles` only searches workspace folders, so also cover a standalone Python file opened without a folder.
return getOpenTextDocuments().some((doc) => doc.languageId === 'python');
}

/**
* Keeps the `python-envs.workspaceHasPython` context key up to date. It controls whether the Python
* activity bar icon and its views are shown.
*/
export function registerWorkspacePythonContext(disposables: Disposable[]): void {
// Every refresh takes a ticket and only the newest one may write the context key, so a slow older
// scan can never overwrite the result of a newer one.
let latestTicket = 0;

const refresh = async (): Promise<void> => {
const ticket = ++latestTicket;
try {
const hasPython = await hasPythonContent();
if (ticket === latestTicket) {
await executeCommand('setContext', PYTHON_WORKSPACE_KEY, hasPython);
}
} catch (ex) {
traceError('Failed to update the Python workspace context', ex);
}
};

const onMarkerChanged = (uri: Uri): void => {
if (!EXCLUDE_RE.test(uri.fsPath)) {
void refresh();
}
};

const watcher = createFileSystemWatcher(MARKER_GLOB, false, true, false);
disposables.push(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is EXCLUDE only passed to findFiles() but not the createFileSystemWatchter? Does that mean the watcher still listens for every .py  file created or deleted under site-packages? I am a bit worried about the perf here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct-- createFileSystemWatcher has no exclude parameter in the VS Code API, so it fires for site-packages too. Fix is to filter in the event handlers:

const EXCLUDE_RE = /[\\/](node_modules|\.git|site-packages)[\\/]/;

watcher.onDidCreate((uri) => { if (!EXCLUDE_RE.test(uri.fsPath)) void refresh(); }),
watcher.onDidDelete((uri) => { if (!EXCLUDE_RE.test(uri.fsPath)) void refresh(); }),

This avoids unnecessary refresh() calls from excluded directories.

watcher,
watcher.onDidCreate(onMarkerChanged),
watcher.onDidDelete(onMarkerChanged),
onDidChangeWorkspaceFolders(() => void refresh()),
onDidOpenTextDocument((doc) => {
if (doc.languageId === 'python') {
void refresh();
}
}),
);
void refresh();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Independent refreshes can overlap, allowing an older findFiles result to overwrite newer workspace state. Serialize or invalidate superseded scans, and add coverage for searches resolving in reverse order.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Each refresh() promise is discarded, so failures from findFiles or setContext can become unhandled rejections. Route these calls through the extension's established async error-reporting or logging pattern.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

This persistent UI policy has no regression coverage. Add focused tests for initial discovery, marker creation and last-marker deletion, exclusions, workspace-folder changes, directory markers, and overlapping refreshes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Could concurrent fire-and-forget refreshes complete out of order and let an older scan overwrite newer workspace state? Serialize refreshes or use a generation token, and cover reverse-order findFiles resolution in a test.

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

This adds asynchronous initial discovery and event-driven context transitions without regression tests. Add coverage for initial state, marker creation, deletion of the final marker, workspace-folder changes, directory markers, and overlapping refreshes.

[verified]

Loading