Skip to content

Commit cdafe7c

Browse files
authored
Merge branch 'main' into simple-roadrunner
2 parents 2a93543 + 10f3105 commit cdafe7c

File tree

15 files changed

+912
-278
lines changed

15 files changed

+912
-278
lines changed

.github/instructions/testing-workflow.instructions.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,20 @@ This guide covers the full testing lifecycle:
3737

3838
These mistakes have occurred REPEATEDLY. Check this list BEFORE writing any test code:
3939

40-
| Mistake | Fix |
41-
| ---------------------------------------------- | ------------------------------------------------------------------ |
42-
| Hardcoded POSIX paths like `'/test/workspace'` | Use `'.'` for relative paths, `Uri.file(x).fsPath` for comparisons |
43-
| Stubbing `workspace.getConfiguration` directly | Stub the wrapper `workspaceApis.getConfiguration` instead |
44-
| Stubbing `workspace.workspaceFolders` property | Stub wrapper function `workspaceApis.getWorkspaceFolders()` |
45-
| Comparing `fsPath` to raw string | Compare `fsPath` to `Uri.file(expected).fsPath` |
40+
| Mistake | Fix |
41+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------ |
42+
| Hardcoded POSIX paths like `'/test/workspace'` | Use `'.'` for relative paths, `Uri.file(x).fsPath` for comparisons |
43+
| Stubbing `workspace.getConfiguration` directly | Stub the wrapper `workspaceApis.getConfiguration` instead |
44+
| Stubbing `workspace.workspaceFolders` property | Stub wrapper function `workspaceApis.getWorkspaceFolders()` |
45+
| Comparing `fsPath` to raw string | Compare `fsPath` to `Uri.file(expected).fsPath` |
46+
| Stubbing `commands.executeCommand` directly | First update production code to use `executeCommand` from `command.api.ts`, then stub that |
47+
| Stubbing `window.createTreeView` directly | First update production code to use `createTreeView` from `window.apis.ts`, then stub that |
4648

4749
**Pre-flight checklist before completing test work:**
4850

4951
- [ ] All paths use `Uri.file().fsPath` (no hardcoded `/path/to/x`)
5052
- [ ] All VS Code API stubs use wrapper modules, not `vscode.*` directly
53+
- [ ] Production code uses wrappers for any VS Code API that tests need to stub (check `src/common/*.apis.ts`)
5154
- [ ] Tests pass on both Windows and POSIX
5255

5356
## Test Types
@@ -597,4 +600,5 @@ envConfig.inspect
597600
- Use `sinon.useFakeTimers()` with `clock.tickAsync()` instead of `await new Promise(resolve => setTimeout(resolve, ms))` for debounce/timeout handling - eliminates flakiness and speeds up tests significantly (1)
598601
- Always compile tests (`npm run compile-tests`) before running them after adding new test cases - test counts will be wrong if running against stale compiled output (1)
599602
- Never create "documentation tests" that just `assert.ok(true)` — if mocking limitations prevent testing, either test a different layer that IS mockable, or skip the test entirely with a clear explanation (1)
600-
- When stubbing vscode APIs in tests via wrapper modules (e.g., `workspaceApis`), the production code must also use those wrappers — sinon cannot stub properties directly on the vscode namespace like `workspace.workspaceFolders`, so both production and test code must reference the same stubbable wrapper functions (3)
603+
- When stubbing vscode APIs in tests via wrapper modules (e.g., `workspaceApis`), the production code must also use those wrappers — sinon cannot stub properties directly on the vscode namespace like `workspace.workspaceFolders`, so both production and test code must reference the same stubbable wrapper functions (4)
604+
- **Before writing tests**, check if the function under test calls VS Code APIs directly (e.g., `commands.executeCommand`, `window.createTreeView`, `workspace.getConfiguration`). If so, FIRST update the production code to use wrapper functions from `src/common/*.apis.ts` (create the wrapper if it doesn't exist), THEN write tests that stub those wrappers. This prevents CI failures where sinon cannot stub the vscode namespace (4)

package.json

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"publisher": "ms-python",
77
"preview": true,
88
"engines": {
9-
"vscode": "^1.110.0-20260204"
9+
"vscode": "^1.110.0-20260204"
1010
},
1111
"categories": [
1212
"Other"
@@ -320,6 +320,12 @@
320320
"category": "Python Envs",
321321
"icon": "$(folder-opened)"
322322
},
323+
{
324+
"command": "python-envs.revealEnvInManagerView",
325+
"title": "%python-envs.revealEnvInManagerView.title%",
326+
"category": "Python Envs",
327+
"icon": "$(eye)"
328+
},
323329
{
324330
"command": "python-envs.runPetInTerminal",
325331
"title": "%python-envs.runPetInTerminal.title%",
@@ -430,6 +436,10 @@
430436
"command": "python-envs.revealProjectInExplorer",
431437
"when": "false"
432438
},
439+
{
440+
"command": "python-envs.revealEnvInManagerView",
441+
"when": "false"
442+
},
433443
{
434444
"command": "python-envs.createNewProjectFromTemplate",
435445
"when": "config.python.useEnvironmentsExtension != false"
@@ -453,6 +463,10 @@
453463
"command": "python-envs.remove",
454464
"when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*;remove;.*/"
455465
},
466+
{
467+
"command": "python-envs.remove",
468+
"when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*;remove;.*/"
469+
},
456470
{
457471
"command": "python-envs.setEnv",
458472
"group": "inline",
@@ -487,6 +501,16 @@
487501
"group": "inline",
488502
"when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/ && viewItem =~ /.*copied.*/"
489503
},
504+
{
505+
"command": "python-envs.copyEnvPath",
506+
"group": "inline",
507+
"when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*/ && viewItem =~ /^((?!copied).)*$/"
508+
},
509+
{
510+
"command": "python-envs.copyEnvPathCopied",
511+
"group": "inline",
512+
"when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*/ && viewItem =~ /.*copied.*/"
513+
},
490514
{
491515
"command": "python-envs.uninstallPackage",
492516
"group": "inline",
@@ -538,6 +562,11 @@
538562
"command": "python-envs.uninstallPackage",
539563
"group": "inline",
540564
"when": "view == python-projects && viewItem == python-package"
565+
},
566+
{
567+
"command": "python-envs.revealEnvInManagerView",
568+
"group": "inline",
569+
"when": "view == python-projects && viewItem == python-env"
541570
}
542571
],
543572
"view/title": [

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal",
4444
"python-envs.uninstallPackage.title": "Uninstall Package",
4545
"python-envs.revealProjectInExplorer.title": "Reveal Project in Explorer",
46+
"python-envs.revealEnvInManagerView.title": "Reveal in Environment Managers View",
4647
"python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal...",
4748
"python-envs.alwaysUseUv.description": "When set to true, uv will be used to manage all virtual environments if available. When set to false, uv will only manage virtual environments explicitly created by uv."
4849
}

src/api.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ export interface PythonEnvironmentInfo {
217217
* Optional `group` for this environment. This is used to group environments in the Environment Manager UI.
218218
*/
219219
readonly group?: string | EnvironmentGroupInfo;
220+
221+
/**
222+
* Error message if the environment is broken or invalid.
223+
* When set, indicates this environment has issues (e.g., broken symlinks, missing Python executable).
224+
* The UI should display a warning indicator and show this message to help users diagnose and fix the issue.
225+
*/
226+
readonly error?: string;
220227
}
221228

222229
/**
@@ -922,7 +929,8 @@ export interface PythonProjectEnvironmentApi {
922929
}
923930

924931
export interface PythonEnvironmentManagerApi
925-
extends PythonEnvironmentManagerRegistrationApi,
932+
extends
933+
PythonEnvironmentManagerRegistrationApi,
926934
PythonEnvironmentItemApi,
927935
PythonEnvironmentManagementApi,
928936
PythonEnvironmentsApi,
@@ -987,7 +995,8 @@ export interface PythonPackageManagementApi {
987995
}
988996

989997
export interface PythonPackageManagerApi
990-
extends PythonPackageManagerRegistrationApi,
998+
extends
999+
PythonPackageManagerRegistrationApi,
9911000
PythonPackageGetterApi,
9921001
PythonPackageManagementApi,
9931002
PythonPackageItemApi {}
@@ -1206,10 +1215,7 @@ export interface PythonBackgroundRunApi {
12061215
}
12071216

12081217
export interface PythonExecutionApi
1209-
extends PythonTerminalCreateApi,
1210-
PythonTerminalRunApi,
1211-
PythonTaskRunApi,
1212-
PythonBackgroundRunApi {}
1218+
extends PythonTerminalCreateApi, PythonTerminalRunApi, PythonTaskRunApi, PythonBackgroundRunApi {}
12131219

12141220
/**
12151221
* Event arguments for when the monitored `.env` files or any other sources change.
@@ -1258,7 +1264,8 @@ export interface PythonEnvironmentVariablesApi {
12581264
* The API for interacting with Python environments, package managers, and projects.
12591265
*/
12601266
export interface PythonEnvironmentApi
1261-
extends PythonEnvironmentManagerApi,
1267+
extends
1268+
PythonEnvironmentManagerApi,
12621269
PythonPackageManagerApi,
12631270
PythonProjectApi,
12641271
PythonExecutionApi,

src/common/window.apis.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
TerminalShellExecutionStartEvent,
2828
TerminalShellIntegrationChangeEvent,
2929
TextEditor,
30+
TreeView,
31+
TreeViewOptions,
3032
Uri,
3133
window,
3234
WindowState,
@@ -377,3 +379,7 @@ export function onDidChangeWindowState(
377379
): Disposable {
378380
return window.onDidChangeWindowState(listener, thisArgs, disposables);
379381
}
382+
383+
export function createTreeView<T>(viewId: string, options: TreeViewOptions<T>): TreeView<T> {
384+
return window.createTreeView(viewId, options);
385+
}

src/extension.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
refreshPackagesCommand,
3636
removeEnvironmentCommand,
3737
removePythonProject,
38+
revealEnvInManagerView,
3839
revealProjectInExplorer,
3940
runAsTaskCommand,
4041
runInDedicatedTerminalCommand,
@@ -180,7 +181,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
180181
}),
181182
commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()),
182183
commands.registerCommand('python-envs.refreshAllManagers', async () => {
183-
await Promise.all(envManagers.managers.map((m) => m.refresh(undefined)));
184+
await window.withProgress({ location: { viewId: 'env-managers' } }, async () => {
185+
await Promise.all(envManagers.managers.map((m) => m.refresh(undefined)));
186+
});
184187
}),
185188
commands.registerCommand('python-envs.searchSettings', async () => {
186189
await openSearchSettings();
@@ -319,6 +322,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
319322
commands.registerCommand('python-envs.revealProjectInExplorer', async (item) => {
320323
await revealProjectInExplorer(item);
321324
}),
325+
commands.registerCommand('python-envs.revealEnvInManagerView', async (item) => {
326+
await revealEnvInManagerView(item, managerView);
327+
}),
322328
commands.registerCommand('python-envs.terminal.activate', async () => {
323329
const terminal = activeTerminal();
324330
if (terminal) {

src/features/envCommands.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import * as fs from 'fs-extra';
22
import * as path from 'path';
3-
import {
4-
commands,
5-
ProgressLocation,
6-
QuickInputButtons,
7-
TaskExecution,
8-
TaskRevealKind,
9-
Terminal,
10-
Uri,
11-
workspace,
12-
} from 'vscode';
3+
import { ProgressLocation, QuickInputButtons, TaskExecution, TaskRevealKind, Terminal, Uri, workspace } from 'vscode';
134
import {
145
CreateEnvironmentOptions,
156
PythonEnvironment,
@@ -28,6 +19,7 @@ import {
2819
} from '../internal.api';
2920
import { removePythonProjectSetting, setEnvironmentManager, setPackageManager } from './settings/settingHelpers';
3021

22+
import { executeCommand } from '../common/command.api';
3123
import { clipboardWriteText } from '../common/env.apis';
3224
import {} from '../common/errors/utils';
3325
import { Pickers } from '../common/localize';
@@ -52,6 +44,7 @@ import {
5244
import { runAsTask } from './execution/runAsTask';
5345
import { runInTerminal } from './terminal/runInTerminal';
5446
import { TerminalManager } from './terminal/terminalManager';
47+
import { EnvManagerView } from './views/envManagersView';
5548
import {
5649
EnvManagerTreeItem,
5750
EnvTreeItemKind,
@@ -469,7 +462,7 @@ export async function addPythonProjectCommand(
469462
'Open Folder',
470463
);
471464
if (r === 'Open Folder') {
472-
await commands.executeCommand('vscode.openFolder');
465+
await executeCommand('vscode.openFolder');
473466
return;
474467
}
475468
}
@@ -746,8 +739,21 @@ export async function copyPathToClipboard(item: unknown): Promise<void> {
746739
export async function revealProjectInExplorer(item: unknown): Promise<void> {
747740
if (item instanceof ProjectItem) {
748741
const projectUri = item.project.uri;
749-
await commands.executeCommand('revealInExplorer', projectUri);
742+
await executeCommand('revealInExplorer', projectUri);
750743
} else {
751744
traceVerbose(`Invalid context for reveal project in explorer: ${item}`);
752745
}
753746
}
747+
748+
/**
749+
* Focuses the Environment Managers view and reveals the given project environment.
750+
*/
751+
export async function revealEnvInManagerView(item: unknown, managerView: EnvManagerView): Promise<void> {
752+
if (item instanceof ProjectEnvironment) {
753+
await executeCommand('env-managers.focus');
754+
await managerView.reveal(item.environment);
755+
return;
756+
}
757+
758+
traceVerbose(`Invalid context for reveal environment in manager view: ${item}`);
759+
}

0 commit comments

Comments
 (0)