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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@
"id": "python",
"title": "Python",
"icon": "files/logo.svg",
"when": "config.python.useEnvironmentsExtension != false"
"when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython"
}
]
},
Expand All @@ -679,14 +679,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
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@
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 { ProjectItem, PythonEnvTreeItem } from './features/views/treeViewItems';

Check failure on line 96 in src/extension.ts

View workflow job for this annotation

GitHub Actions / Lint

'ProjectItem' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 96 in src/extension.ts

View workflow job for this annotation

GitHub Actions / TypeScript Unit Tests (windows-latest)

'ProjectItem' is declared but its value is never read.

Check failure on line 96 in src/extension.ts

View workflow job for this annotation

GitHub Actions / TypeScript Unit Tests (ubuntu-latest)

'ProjectItem' is declared but its value is never read.

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

ProjectItem is newly imported but is not used by this change. Remove the import to avoid an unused-local lint or compile failure.

import { registerWorkspacePythonContext } from './features/views/workspacePythonContext';
import { collectEnvironmentInfo, getEnvManagerAndPackageManagerConfigLevels, runPetInTerminalImpl } from './helpers';
import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api';
import { registerInlineScriptFeatures } from './managers/builtin/inlineScript/main';
Expand All @@ -113,6 +114,7 @@
import { registerPyenvFeatures } from './managers/pyenv/main';

export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi | undefined> {
registerWorkspacePythonContext(context.subscriptions);
// Only skip activation if user explicitly set useEnvironmentsExtension to false.

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

Registration occurs before the useEnvironmentsExtension === false activation exit, so the disabled feature still creates a watcher and initiates discovery. Move registration after the configuration guard; the contribution-level condition already keeps the UI hidden.

// When disabled, the main Python extension handles environments instead (legacy mode).
const config = getConfiguration('python');
Expand Down
24 changes: 24 additions & 0 deletions src/features/views/workspacePythonContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Disposable } from 'vscode';
import { executeCommand } from '../../common/command.api';
import { createFileSystemWatcher, findFiles, onDidChangeWorkspaceFolders } from '../../common/workspace.apis';

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

const MARKER_GLOB = '**/{*.py,pyproject.toml,setup.py,requirements.txt,Pipfile,manage.py,app.py,.venv,.conda,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.


async function refresh(): Promise<void> {
const hits = await findFiles(MARKER_GLOB, EXCLUDE, 1);

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 this only searching inside open workspaces folders. What if user opens a standalone Python file without opening the folder?

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.

findFiles only searches workspace folders, so standalone file case is missed. Will add a fallback check on open text documents:

async function refresh(): Promise<void> {
    const hits = await findFiles(MARKER_GLOB, EXCLUDE, 1);
    if (hits.length > 0) {
        await executeCommand('setContext', PYTHON_WORKSPACE_KEY, true);
        return;
    }
    const hasPythonDoc = workspace.textDocuments.some(
        (doc) => doc.languageId === 'python',
    );
    await executeCommand('setContext', PYTHON_WORKSPACE_KEY, hasPythonDoc);
}

And subscribe to onDidOpenTextDocument in registerWorkspacePythonContext.

await executeCommand('setContext', PYTHON_WORKSPACE_KEY, hits.length > 0);
}

export function registerWorkspacePythonContext(disposables: Disposable[]): void {
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(() => void refresh()),
watcher.onDidDelete(() => void refresh()),
onDidChangeWorkspaceFolders(() => 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