Skip to content

Commit 88880b7

Browse files
authored
Merge branch 'main' into notebook-path-fix
2 parents 1b36b03 + b71dfe0 commit 88880b7

File tree

18 files changed

+351
-98
lines changed

18 files changed

+351
-98
lines changed

README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ The Python Environments and Package Manager extension for VS Code helps you mana
1212

1313
<img src=https://raw.githubusercontent.com/microsoft/vscode-python-environments/main/images/python-envs-overview.gif width=734 height=413>
1414

15-
1615
### Environment Management
1716

1817
This extension provides an Environments view, which can be accessed via the VS Code Activity Bar, where you can manage your Python environments. Here, you can create, delete, and switch between environments, as well as install and uninstall packages within the selected environment. It also provides APIs for extension developers to contribute their own environment managers.
@@ -54,19 +53,44 @@ See [api.ts](https://github.com/microsoft/vscode-python-environments/blob/main/s
5453
To consume these APIs you can look at the example here:
5554
https://github.com/microsoft/vscode-python-environments/blob/main/examples/README.md
5655

56+
### Callable Commands
57+
58+
The extension provides a set of callable commands that can be used to interact with the environment and package managers. These commands can be invoked from other extensions or from the command palette.
59+
60+
#### `python-envs.createAny`
61+
62+
Create a new environment using any of the available environment managers. This command will prompt the user to select the environment manager to use. Following options are available on this command:
63+
64+
```typescript
65+
{
66+
/**
67+
* Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput.
68+
*/
69+
showBackButton?: boolean;
70+
71+
/**
72+
* Default `true`. If `true`, the environment after creation will be selected.
73+
*/
74+
selectEnvironment?: boolean;
75+
}
76+
```
77+
78+
usage: `await vscode.commands.executeCommand('python-envs.createAny', options);`
5779

5880
## Extension Dependency
5981

60-
This section provides an overview of how the Python extension interacts with the Python Environments extension and other tool-specific extensions. The Python Environments extension allows users to create, manage, and remove Python environments and packages. It also provides an API that other extensions can use to support environment management or consume it for running Python tools or projects.
82+
This section provides an overview of how the Python extension interacts with the Python Environments extension and other tool-specific extensions. The Python Environments extension allows users to create, manage, and remove Python environments and packages. It also provides an API that other extensions can use to support environment management or consume it for running Python tools or projects.
6183

6284
Tools that may rely on these APIs in their own extensions include:
63-
- **Debuggers** (e.g., `debugpy`)
64-
- **Linters** (e.g., Pylint, Flake8, Mypy)
65-
- **Formatters** (e.g., Black, autopep8)
66-
- **Language Server extensions** (e.g., Pylance, Jedi)
67-
- **Environment and Package Manager extensions** (e.g., Pixi, Conda, Hatch)
85+
86+
- **Debuggers** (e.g., `debugpy`)
87+
- **Linters** (e.g., Pylint, Flake8, Mypy)
88+
- **Formatters** (e.g., Black, autopep8)
89+
- **Language Server extensions** (e.g., Pylance, Jedi)
90+
- **Environment and Package Manager extensions** (e.g., Pixi, Conda, Hatch)
6891

6992
### API Dependency
93+
7094
The relationship between these extensions can be represented as follows:
7195

7296
<img src=https://raw.githubusercontent.com/microsoft/vscode-python-environments/refs/heads/main/images/extension_relationships.png width=734 height=413>
@@ -75,7 +99,7 @@ Users who do not need to execute code or work in **Virtual Workspaces** can use
7599

76100
### Trust Relationship Between Python and Python Environments Extensions
77101

78-
VS Code supports trust management, allowing extensions to function in either **trusted** or **untrusted** scenarios. Code execution and tools that can modify the user’s environment are typically unavailable in untrusted scenarios.
102+
VS Code supports trust management, allowing extensions to function in either **trusted** or **untrusted** scenarios. Code execution and tools that can modify the user’s environment are typically unavailable in untrusted scenarios.
79103

80104
The relationship is illustrated below:
81105

src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ export interface PackageManager {
596596
/**
597597
* Installs/Uninstall packages in the specified Python environment.
598598
* @param environment - The Python environment in which to install packages.
599-
* @param packages - The packages to install.
599+
* @param options - Options for managing packages.
600600
* @returns A promise that resolves when the installation is complete.
601601
*/
602602
manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise<void>;

src/common/pickers/managers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function getDescription(mgr: InternalEnvironmentManager | InternalPackageManager
2121
export async function pickEnvironmentManager(
2222
managers: InternalEnvironmentManager[],
2323
defaultManagers?: InternalEnvironmentManager[],
24+
showBackButton?: boolean,
2425
): Promise<string | undefined> {
2526
if (managers.length === 0) {
2627
return;
@@ -72,6 +73,7 @@ export async function pickEnvironmentManager(
7273
const item = await showQuickPickWithButtons(items, {
7374
placeHolder: Pickers.Managers.selectEnvironmentManager,
7475
ignoreFocusOut: true,
76+
showBackButton,
7577
});
7678
return (item as QuickPickItem & { id: string })?.id;
7779
}

src/common/telemetry/constants.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ export enum EventNames {
44

55
ENVIRONMENT_MANAGER_REGISTERED = 'ENVIRONMENT_MANAGER.REGISTERED',
66
PACKAGE_MANAGER_REGISTERED = 'PACKAGE_MANAGER.REGISTERED',
7+
ENVIRONMENT_MANAGER_SELECTED = 'ENVIRONMENT_MANAGER.SELECTED',
8+
PACKAGE_MANAGER_SELECTED = 'PACKAGE_MANAGER.SELECTED',
79

810
VENV_USING_UV = 'VENV.USING_UV',
911
VENV_CREATION = 'VENV.CREATION',
12+
13+
PACKAGE_MANAGEMENT = 'PACKAGE_MANAGEMENT',
1014
}
1115

1216
// Map all events to their properties
@@ -43,16 +47,43 @@ export interface IEventNamePropertyMapping {
4347
};
4448

4549
/* __GDPR__
46-
"venv.using_uv": {"owner": "karthiknadig" }
50+
"environment_manager.selected": {
51+
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
52+
}
53+
*/
54+
[EventNames.ENVIRONMENT_MANAGER_SELECTED]: {
55+
managerId: string;
56+
};
57+
58+
/* __GDPR__
59+
"package_manager.selected": {
60+
"managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
61+
}
4762
*/
48-
[EventNames.VENV_USING_UV]: never | undefined;
63+
[EventNames.PACKAGE_MANAGER_SELECTED]: {
64+
managerId: string;
65+
};
4966

5067
/* __GDPR__
68+
"venv.using_uv": {"owner": "karthiknadig" }
69+
*/
70+
[EventNames.VENV_USING_UV]: never | undefined /* __GDPR__
5171
"venv.creation": {
5272
"creationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
5373
}
54-
*/
74+
*/;
5575
[EventNames.VENV_CREATION]: {
5676
creationType: 'quick' | 'custom';
5777
};
78+
79+
/* __GDPR__
80+
"package.install": {
81+
"managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" },
82+
"result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }
83+
}
84+
*/
85+
[EventNames.PACKAGE_MANAGEMENT]: {
86+
managerId: string;
87+
result: 'success' | 'error' | 'cancelled';
88+
};
5889
}

src/common/telemetry/helpers.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers';
2+
import { PythonProjectManager } from '../../internal.api';
3+
import { EventNames } from './constants';
4+
import { sendTelemetryEvent } from './sender';
5+
6+
export function sendManagerSelectionTelemetry(pm: PythonProjectManager) {
7+
const ems: Set<string> = new Set();
8+
const ps: Set<string> = new Set();
9+
pm.getProjects().forEach((project) => {
10+
const m = getDefaultEnvManagerSetting(pm, project.uri);
11+
if (m) {
12+
ems.add(m);
13+
}
14+
15+
const p = getDefaultPkgManagerSetting(pm, project.uri);
16+
if (p) {
17+
ps.add(p);
18+
}
19+
});
20+
21+
ems.forEach((em) => {
22+
sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_SELECTED, undefined, { managerId: em });
23+
});
24+
25+
ps.forEach((pkg) => {
26+
sendTelemetryEvent(EventNames.PACKAGE_MANAGER_SELECTED, undefined, { managerId: pkg });
27+
});
28+
}

src/extension.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { commands, ExtensionContext, LogOutputChannel, Terminal, Uri } from 'vscode';
22

33
import { PythonEnvironmentManagers } from './features/envManagers';
4-
import { registerLogger, traceInfo } from './common/logging';
4+
import { registerLogger, traceError, traceInfo } from './common/logging';
55
import { EnvManagerView } from './features/views/envManagersView';
66
import {
77
addPythonProject,
@@ -56,6 +56,7 @@ import { registerTools } from './common/lm.apis';
5656
import { GetEnvironmentInfoTool, InstallPackageTool } from './features/copilotTools';
5757
import { TerminalActivationImpl } from './features/terminal/terminalActivationState';
5858
import { getEnvironmentForTerminal } from './features/terminal/utils';
59+
import { sendManagerSelectionTelemetry } from './common/telemetry/helpers';
5960

6061
export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
6162
const start = new StopWatch();
@@ -88,7 +89,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
8889
const projectCreators: ProjectCreators = new ProjectCreatorsImpl();
8990
context.subscriptions.push(
9091
projectCreators,
91-
projectCreators.registerPythonProjectCreator(new ExistingProjects()),
92+
projectCreators.registerPythonProjectCreator(new ExistingProjects(projectManager)),
9293
projectCreators.registerPythonProjectCreator(new AutoFindProjects(projectManager)),
9394
);
9495

@@ -138,7 +139,11 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
138139
envManagers,
139140
projectManager,
140141
);
141-
packageManager.manage(environment, { install: [] });
142+
try {
143+
packageManager.manage(environment, { install: [] });
144+
} catch (err) {
145+
traceError('Error when running command python-envs.packages', err);
146+
}
142147
}),
143148
commands.registerCommand('python-envs.uninstallPackage', async (context: unknown) => {
144149
await handlePackageUninstall(context, envManagers);
@@ -263,8 +268,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
263268
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel),
264269
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
265270
]);
271+
266272
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
267273
await terminalManager.initialize(api);
274+
sendManagerSelectionTelemetry(projectManager);
268275
});
269276

270277
sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime);

src/features/creators/autoFindProjects.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as path from 'path';
22
import { Uri } from 'vscode';
3-
import { showQuickPickWithButtons } from '../../common/window.apis';
3+
import { showQuickPickWithButtons, showWarningMessage } from '../../common/window.apis';
44
import { ProjectCreatorString } from '../../common/localize';
55
import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
66
import { PythonProjectManager } from '../../internal.api';
77
import { showErrorMessage } from '../../common/errors/utils';
88
import { findFiles } from '../../common/workspace.apis';
9+
import { traceInfo } from '../../common/logging';
910

1011
function getUniqueUri(uris: Uri[]): {
1112
label: string;
@@ -68,8 +69,9 @@ export class AutoFindProjects implements PythonProjectCreator {
6869
const filtered = files.filter((uri) => {
6970
const p = this.pm.get(uri);
7071
if (p) {
71-
// If there ia already a project with the same path, skip it.
72-
// If there is a project with the same parent path, skip it.
72+
// Skip this project if:
73+
// 1. There's already a project registered with exactly the same path
74+
// 2. There's already a project registered with this project's parent directory path
7375
const np = path.normalize(p.uri.fsPath);
7476
const nf = path.normalize(uri.fsPath);
7577
const nfp = path.dirname(nf);
@@ -79,11 +81,20 @@ export class AutoFindProjects implements PythonProjectCreator {
7981
});
8082

8183
if (filtered.length === 0) {
84+
// No new projects found that are not already in the project manager
85+
traceInfo('All discovered projects are already registered in the project manager');
86+
setImmediate(() => {
87+
showWarningMessage('No new projects found');
88+
});
8289
return;
8390
}
8491

92+
traceInfo(`Found ${filtered.length} new potential projects that aren't already registered`);
93+
8594
const projects = await pickProjects(filtered);
8695
if (!projects || projects.length === 0) {
96+
// User cancelled the selection.
97+
traceInfo('User cancelled project selection.');
8798
return;
8899
}
89100

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import * as path from 'path';
22
import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
33
import { ProjectCreatorString } from '../../common/localize';
4-
import { showOpenDialog } from '../../common/window.apis';
4+
import { showOpenDialog, showWarningMessage } from '../../common/window.apis';
5+
import { PythonProjectManager } from '../../internal.api';
6+
import { traceInfo } from '../../common/logging';
7+
import { Uri, window, workspace } from 'vscode';
8+
import { traceLog } from '../../common/logging';
59

610
export class ExistingProjects implements PythonProjectCreator {
711
public readonly name = 'existingProjects';
812
public readonly displayName = ProjectCreatorString.addExistingProjects;
913

14+
constructor(private readonly pm: PythonProjectManager) {}
15+
1016
async create(_options?: PythonProjectCreatorOptions): Promise<PythonProject | PythonProject[] | undefined> {
1117
const results = await showOpenDialog({
1218
canSelectFiles: true,
@@ -19,12 +25,73 @@ export class ExistingProjects implements PythonProjectCreator {
1925
});
2026

2127
if (!results || results.length === 0) {
28+
// User cancelled the dialog & doesn't want to add any projects
2229
return;
2330
}
2431

25-
return results.map((r) => ({
26-
name: path.basename(r.fsPath),
27-
uri: r,
28-
}));
32+
// do we have any limitations that need to be applied here?
33+
// like selected folder not child of workspace folder?
34+
35+
const filtered = results.filter((uri) => {
36+
const p = this.pm.get(uri);
37+
if (p) {
38+
// Skip this project if there's already a project registered with exactly the same path
39+
const np = path.normalize(p.uri.fsPath);
40+
const nf = path.normalize(uri.fsPath);
41+
return np !== nf;
42+
}
43+
return true;
44+
});
45+
46+
if (filtered.length === 0) {
47+
// No new projects found that are not already in the project manager
48+
traceInfo('All discovered projects are already registered in the project manager');
49+
setImmediate(() => {
50+
showWarningMessage('No new projects found');
51+
});
52+
return;
53+
}
54+
55+
// for all the selected files / folders, check to make sure they are in the workspace
56+
const resultsOutsideWorkspace: Uri[] = [];
57+
const workspaceRoots: Uri[] = workspace.workspaceFolders?.map((w) => w.uri) || [];
58+
const resultsInWorkspace = filtered.filter((r) => {
59+
const exists = workspaceRoots.some((w) => r.fsPath.startsWith(w.fsPath));
60+
if (!exists) {
61+
traceLog(`File ${r.fsPath} is not in the workspace, ignoring it from 'add projects' list.`);
62+
resultsOutsideWorkspace.push(r);
63+
}
64+
return exists;
65+
});
66+
if (resultsInWorkspace.length === 0) {
67+
// Show a single error message with option to add to workspace
68+
const response = await window.showErrorMessage(
69+
'Selected items are not in the current workspace.',
70+
'Add to Workspace',
71+
'Cancel',
72+
);
73+
74+
if (response === 'Add to Workspace') {
75+
// Use the command palette to let user adjust which folders to add
76+
// Add folders programmatically using workspace API
77+
for (const r of resultsOutsideWorkspace) {
78+
// if the user selects a file, add that file to the workspace
79+
await // if the user selects a folder, add that folder to the workspace
80+
await workspace.updateWorkspaceFolders(
81+
workspace.workspaceFolders?.length || 0, // Start index
82+
0, // Delete count
83+
{
84+
uri: r,
85+
},
86+
);
87+
}
88+
}
89+
return;
90+
} else {
91+
return resultsInWorkspace.map((uri) => ({
92+
name: path.basename(uri.fsPath),
93+
uri,
94+
})) as PythonProject[];
95+
}
2996
}
3097
}

0 commit comments

Comments
 (0)