Repository navigation
fix: hide Python activity bar icon in non-Python workspaces #1663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
88f9c13
96e6814
293f0ec
96e0494
f922e06
d72d5df
df4e496
5a0a982
d1503b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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}/**'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The classifier omits legitimate notebook-only and stub-only workspaces ( |
||
| // `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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct-- 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 |
||
| watcher, | ||
| watcher.onDidCreate(onMarkerChanged), | ||
| watcher.onDidDelete(onMarkerChanged), | ||
| onDidChangeWorkspaceFolders(() => void refresh()), | ||
| onDidOpenTextDocument((doc) => { | ||
| if (doc.languageId === 'python') { | ||
| void refresh(); | ||
| } | ||
| }), | ||
| ); | ||
| void refresh(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Independent refreshes can overlap, allowing an older There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Each There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 |
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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] |
||
There was a problem hiding this comment.
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.workspaceHasPythonThat key is not defined initially. It only gets set after the extension’s
activate()function runs:registerWorkspacePythonContext(context.subscriptions);However,
package.jsoncurrently activates the extension only when VS Code opens a Python-language document:"activationEvents": [ "onLanguage:python" ]This creates a circular dependency:
onLanguage:pythonnever 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.
There was a problem hiding this comment.
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
workspaceContainsactivation events topackage.jsonso the extension activates even before a.pyfile 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.