Skip to content

Commit 69500c5

Browse files
authored
Merge branch 'main' into main
2 parents f07ff59 + dbc5656 commit 69500c5

12 files changed

Lines changed: 390 additions & 22 deletions

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"url": "https://github.com/microsoft/vscode-python-environments/issues"
3737
},
3838
"main": "./dist/extension.js",
39+
"l10n": "./l10n",
3940
"icon": "icon.png",
4041
"contributes": {
4142
"configuration": {

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@
3636
"python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal",
3737
"python-envs.uninstallPackage.title": "Uninstall Package",
3838
"python-envs.revealProjectInExplorer.title": "Reveal Project in Explorer",
39-
"python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal"
39+
"python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal..."
4040
}

src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1223,7 +1223,7 @@ export interface DidChangeEnvironmentVariablesEventArgs {
12231223
/**
12241224
* The type of change that occurred.
12251225
*/
1226-
changeTye: FileChangeType;
1226+
changeType: FileChangeType;
12271227
}
12281228

12291229
export interface PythonEnvironmentVariablesApi {

src/extension.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { EventNames } from './common/telemetry/constants';
99
import { sendManagerSelectionTelemetry } from './common/telemetry/helpers';
1010
import { sendTelemetryEvent } from './common/telemetry/sender';
1111
import { createDeferred } from './common/utils/deferred';
12+
import { isWindows } from './common/utils/platformUtils';
1213
import {
1314
activeTerminal,
1415
createLogOutputChannel,
@@ -57,6 +58,7 @@ import {
5758
import { ShellStartupActivationVariablesManagerImpl } from './features/terminal/shellStartupActivationVariablesManager';
5859
import { cleanupStartupScripts } from './features/terminal/shellStartupSetupHandlers';
5960
import { TerminalActivationImpl } from './features/terminal/terminalActivationState';
61+
import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInjector';
6062
import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager';
6163
import { getAutoActivationType, getEnvironmentForTerminal } from './features/terminal/utils';
6264
import { EnvManagerView } from './features/views/envManagersView';
@@ -239,6 +241,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
239241
api,
240242
);
241243

244+
// Initialize terminal environment variable injection
245+
const terminalEnvVarInjector = new TerminalEnvVarInjector(context.environmentVariableCollection, envVarManager);
246+
context.subscriptions.push(terminalEnvVarInjector);
247+
242248
context.subscriptions.push(
243249
shellStartupVarsMgr,
244250
registerCompletionProvider(envManagers),
@@ -442,12 +448,63 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
442448
commands.registerCommand('python-envs.runPetInTerminal', async () => {
443449
try {
444450
const petPath = await getNativePythonToolsPath();
451+
452+
// Show quick pick menu for PET operation selection
453+
const selectedOption = await window.showQuickPick(
454+
[
455+
{
456+
label: 'Find All Environments',
457+
description: 'Finds all environments and reports them to the standard output',
458+
detail: 'Runs: pet find --verbose',
459+
},
460+
{
461+
label: 'Resolve Environment...',
462+
description: 'Resolves & reports the details of the environment to the standard output',
463+
detail: 'Runs: pet resolve <path>',
464+
},
465+
],
466+
{
467+
placeHolder: 'Select a Python Environment Tool (PET) operation',
468+
ignoreFocusOut: true,
469+
},
470+
);
471+
472+
if (!selectedOption) {
473+
return; // User cancelled
474+
}
475+
445476
const terminal = createTerminal({
446477
name: 'Python Environment Tool (PET)',
447478
});
448479
terminal.show();
449-
terminal.sendText(`"${petPath}"`, true);
450-
traceInfo(`Running PET in terminal: ${petPath}`);
480+
481+
if (selectedOption.label === 'Find All Environments') {
482+
// Run pet find --verbose
483+
terminal.sendText(`"${petPath}" find --verbose`, true);
484+
traceInfo(`Running PET find command: ${petPath} find --verbose`);
485+
} else if (selectedOption.label === 'Resolve Environment...') {
486+
// Show input box for path
487+
const placeholder = isWindows() ? 'C:\\path\\to\\python\\executable' : '/path/to/python/executable';
488+
const inputPath = await window.showInputBox({
489+
prompt: 'Enter the path to the Python executable to resolve',
490+
placeHolder: placeholder,
491+
ignoreFocusOut: true,
492+
validateInput: (value) => {
493+
if (!value || value.trim().length === 0) {
494+
return 'Please enter a valid path';
495+
}
496+
return null;
497+
},
498+
});
499+
500+
if (!inputPath) {
501+
return; // User cancelled
502+
}
503+
504+
// Run pet resolve with the provided path
505+
terminal.sendText(`"${petPath}" resolve "${inputPath.trim()}"`, true);
506+
traceInfo(`Running PET resolve command: ${petPath} resolve "${inputPath.trim()}"`);
507+
}
451508
} catch (error) {
452509
traceError('Error running PET in terminal', error);
453510
window.showErrorMessage(`Failed to run Python Environment Tool: ${error}`);

src/features/execution/envVariableManager.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import * as path from 'path';
21
import * as fsapi from 'fs-extra';
3-
import { Uri, Event, EventEmitter, FileChangeType } from 'vscode';
4-
import { DidChangeEnvironmentVariablesEventArgs, PythonEnvironmentVariablesApi } from '../../api';
2+
import * as path from 'path';
3+
import { Event, EventEmitter, FileChangeType, Uri } from 'vscode';
54
import { Disposable } from 'vscode-jsonrpc';
5+
import { DidChangeEnvironmentVariablesEventArgs, PythonEnvironmentVariablesApi } from '../../api';
6+
import { resolveVariables } from '../../common/utils/internalVariables';
67
import { createFileSystemWatcher, getConfiguration } from '../../common/workspace.apis';
78
import { PythonProjectManager } from '../../internal.api';
89
import { mergeEnvVariables, parseEnvFile } from './envVarUtils';
9-
import { resolveVariables } from '../../common/utils/internalVariables';
1010

1111
export interface EnvVarManager extends PythonEnvironmentVariablesApi, Disposable {}
1212

@@ -25,13 +25,13 @@ export class PythonEnvVariableManager implements EnvVarManager {
2525
this._onDidChangeEnvironmentVariables,
2626
this.watcher,
2727
this.watcher.onDidCreate((e) =>
28-
this._onDidChangeEnvironmentVariables.fire({ uri: e, changeTye: FileChangeType.Created }),
28+
this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Created }),
2929
),
3030
this.watcher.onDidChange((e) =>
31-
this._onDidChangeEnvironmentVariables.fire({ uri: e, changeTye: FileChangeType.Changed }),
31+
this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Changed }),
3232
),
3333
this.watcher.onDidDelete((e) =>
34-
this._onDidChangeEnvironmentVariables.fire({ uri: e, changeTye: FileChangeType.Deleted }),
34+
this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Deleted }),
3535
),
3636
);
3737
}
@@ -48,7 +48,7 @@ export class PythonEnvVariableManager implements EnvVarManager {
4848

4949
const config = getConfiguration('python', project?.uri ?? uri);
5050
let envFilePath = config.get<string>('envFile');
51-
envFilePath = envFilePath ? path.normalize(resolveVariables(envFilePath)) : undefined;
51+
envFilePath = envFilePath ? path.normalize(resolveVariables(envFilePath, uri)) : undefined;
5252

5353
if (envFilePath && (await fsapi.pathExists(envFilePath))) {
5454
const other = await parseEnvFile(Uri.file(envFilePath));
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as fse from 'fs-extra';
5+
import * as path from 'path';
6+
import {
7+
Disposable,
8+
EnvironmentVariableScope,
9+
GlobalEnvironmentVariableCollection,
10+
workspace,
11+
WorkspaceFolder,
12+
} from 'vscode';
13+
import { traceError, traceVerbose } from '../../common/logging';
14+
import { resolveVariables } from '../../common/utils/internalVariables';
15+
import { getConfiguration, getWorkspaceFolder } from '../../common/workspace.apis';
16+
import { EnvVarManager } from '../execution/envVariableManager';
17+
18+
/**
19+
* Manages injection of workspace-specific environment variables into VS Code terminals
20+
* using the GlobalEnvironmentVariableCollection API.
21+
*/
22+
export class TerminalEnvVarInjector implements Disposable {
23+
private disposables: Disposable[] = [];
24+
25+
constructor(
26+
private readonly envVarCollection: GlobalEnvironmentVariableCollection,
27+
private readonly envVarManager: EnvVarManager,
28+
) {
29+
this.initialize();
30+
}
31+
32+
/**
33+
* Initialize the injector by setting up watchers and injecting initial environment variables.
34+
*/
35+
private async initialize(): Promise<void> {
36+
traceVerbose('TerminalEnvVarInjector: Initializing environment variable injection');
37+
38+
// Listen for environment variable changes from the manager
39+
this.disposables.push(
40+
this.envVarManager.onDidChangeEnvironmentVariables((args) => {
41+
if (!args.uri) {
42+
// No specific URI, reload all workspaces
43+
this.updateEnvironmentVariables().catch((error) => {
44+
traceError('Failed to update environment variables:', error);
45+
});
46+
return;
47+
}
48+
49+
const affectedWorkspace = getWorkspaceFolder(args.uri);
50+
if (!affectedWorkspace) {
51+
// No workspace folder found for this URI, reloading all workspaces
52+
this.updateEnvironmentVariables().catch((error) => {
53+
traceError('Failed to update environment variables:', error);
54+
});
55+
return;
56+
}
57+
58+
if (args.changeType === 2) {
59+
// FileChangeType.Deleted
60+
this.clearWorkspaceVariables(affectedWorkspace);
61+
} else {
62+
this.updateEnvironmentVariables(affectedWorkspace).catch((error) => {
63+
traceError('Failed to update environment variables:', error);
64+
});
65+
}
66+
}),
67+
);
68+
69+
// Initial load of environment variables
70+
await this.updateEnvironmentVariables();
71+
}
72+
73+
/**
74+
* Update environment variables in the terminal collection.
75+
*/
76+
private async updateEnvironmentVariables(workspaceFolder?: WorkspaceFolder): Promise<void> {
77+
try {
78+
if (workspaceFolder) {
79+
// Update only the specified workspace
80+
traceVerbose(
81+
`TerminalEnvVarInjector: Updating environment variables for workspace: ${workspaceFolder.uri.fsPath}`,
82+
);
83+
await this.injectEnvironmentVariablesForWorkspace(workspaceFolder);
84+
} else {
85+
// No provided workspace - update all workspaces
86+
this.envVarCollection.clear();
87+
88+
const workspaceFolders = workspace.workspaceFolders;
89+
if (!workspaceFolders || workspaceFolders.length === 0) {
90+
traceVerbose('TerminalEnvVarInjector: No workspace folders found, skipping env var injection');
91+
return;
92+
}
93+
94+
traceVerbose('TerminalEnvVarInjector: Updating environment variables for all workspaces');
95+
for (const folder of workspaceFolders) {
96+
await this.injectEnvironmentVariablesForWorkspace(folder);
97+
}
98+
}
99+
100+
traceVerbose('TerminalEnvVarInjector: Environment variable injection completed');
101+
} catch (error) {
102+
traceError('TerminalEnvVarInjector: Error updating environment variables:', error);
103+
}
104+
}
105+
106+
/**
107+
* Inject environment variables for a specific workspace.
108+
*/
109+
private async injectEnvironmentVariablesForWorkspace(workspaceFolder: WorkspaceFolder): Promise<void> {
110+
const workspaceUri = workspaceFolder.uri;
111+
try {
112+
const envVars = await this.envVarManager.getEnvironmentVariables(workspaceUri);
113+
114+
// use scoped environment variable collection
115+
const envVarScope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder });
116+
envVarScope.clear(); // Clear existing variables for this workspace
117+
118+
// Track which .env file is being used for logging
119+
const config = getConfiguration('python', workspaceUri);
120+
const envFilePath = config.get<string>('envFile');
121+
const resolvedEnvFilePath: string | undefined = envFilePath
122+
? path.resolve(resolveVariables(envFilePath, workspaceUri))
123+
: undefined;
124+
const defaultEnvFilePath: string = path.join(workspaceUri.fsPath, '.env');
125+
126+
let activeEnvFilePath: string = resolvedEnvFilePath || defaultEnvFilePath;
127+
if (activeEnvFilePath && (await fse.pathExists(activeEnvFilePath))) {
128+
traceVerbose(`TerminalEnvVarInjector: Using env file: ${activeEnvFilePath}`);
129+
} else {
130+
traceVerbose(
131+
`TerminalEnvVarInjector: No .env file found for workspace: ${workspaceUri.fsPath}, not injecting environment variables.`,
132+
);
133+
return; // No .env file to inject
134+
}
135+
136+
for (const [key, value] of Object.entries(envVars)) {
137+
if (value === undefined) {
138+
// Remove the environment variable if the value is undefined
139+
envVarScope.delete(key);
140+
} else {
141+
envVarScope.replace(key, value);
142+
}
143+
}
144+
} catch (error) {
145+
traceError(
146+
`TerminalEnvVarInjector: Error injecting environment variables for workspace ${workspaceUri.fsPath}:`,
147+
error,
148+
);
149+
}
150+
}
151+
152+
/**
153+
* Dispose of the injector and clean up resources.
154+
*/
155+
dispose(): void {
156+
traceVerbose('TerminalEnvVarInjector: Disposing');
157+
this.disposables.forEach((disposable) => disposable.dispose());
158+
this.disposables = [];
159+
160+
// Clear all environment variables from the collection
161+
this.envVarCollection.clear();
162+
}
163+
164+
private getEnvironmentVariableCollectionScoped(scope: EnvironmentVariableScope = {}) {
165+
const envVarCollection = this.envVarCollection as GlobalEnvironmentVariableCollection;
166+
return envVarCollection.getScoped(scope);
167+
}
168+
169+
/**
170+
* Clear all environment variables for a workspace.
171+
*/
172+
private clearWorkspaceVariables(workspaceFolder: WorkspaceFolder): void {
173+
try {
174+
const scope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder });
175+
scope.clear();
176+
} catch (error) {
177+
traceError(`Failed to clear environment variables for workspace ${workspaceFolder.uri.fsPath}:`, error);
178+
}
179+
}
180+
}

src/managers/builtin/pipListUtils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ export interface PipPackage {
44
displayName: string;
55
description: string;
66
}
7+
export function isValidVersion(version: string): boolean {
8+
return /^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$/.test(
9+
version,
10+
);
11+
}
712
export function parsePipList(data: string): PipPackage[] {
813
const collection: PipPackage[] = [];
914

@@ -13,9 +18,12 @@ export function parsePipList(data: string): PipPackage[] {
1318
continue;
1419
}
1520
const parts = line.split(' ').filter((e) => e);
16-
if (parts.length > 1) {
21+
if (parts.length === 2) {
1722
const name = parts[0].trim();
1823
const version = parts[1].trim();
24+
if (!isValidVersion(version)) {
25+
continue;
26+
}
1927
const pkg = {
2028
name,
2129
version,

0 commit comments

Comments
 (0)