Skip to content
Merged
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}/**';
Comment thread
edvilme marked this conversation as resolved.
// `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();
Comment thread
edvilme marked this conversation as resolved.
Comment thread
edvilme marked this conversation as resolved.
Comment thread
edvilme marked this conversation as resolved.
Comment thread
edvilme marked this conversation as resolved.
}
Comment thread
edvilme marked this conversation as resolved.
Loading
Loading