Skip to content
Closed
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ node_modules
*.vsix
.nox/
.venv/
**/__pycache__/
**/__pycache__/

# Folder for storing AI generated artifacts
ai-artifacts/*
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
"python-envs.workspaceSearchPaths": {
"type": "array",
"description": "%python-envs.workspaceSearchPaths.description%",
"default": [],
"default": ["./**/.venv"],
"scope": "resource",
"items": {
"type": "string"
Expand Down Expand Up @@ -213,6 +213,12 @@
"category": "Python",
"icon": "$(refresh)"
},
{
"command": "python-envs.searchSettings",
"title": "%python-envs.searchSettings.title%",
"category": "Python",
"icon": "$(gear)"
},
{
"command": "python-envs.refreshPackages",
"title": "%python-envs.refreshPackages.title%",
Expand Down Expand Up @@ -574,6 +580,11 @@
"group": "navigation",
"when": "view == env-managers"
},
{
"command": "python-envs.searchSettings",
"group": "navigation",
"when": "view == env-managers"
},
{
"command": "python-envs.refreshAllManagers",
"group": "navigation",
Expand Down
5 changes: 3 additions & 2 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"python-envs.terminal.autoActivationType.shellStartup": "Activation using [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) or by modifying the terminal shell startup script. Enable `terminal.integrated.shellIntegration.enabled` or we may need to modify your shell startup scripts for the ideal experience.",
"python-envs.terminal.autoActivationType.off": "No automatic activation of environments.",
"python-envs.terminal.useEnvFile.description": "Controls whether environment variables from .env files and python.envFile setting are injected into terminals.",
"python-envs.globalSearchPaths.description": "Global search paths for Python environments. Absolute directory paths that are searched at the user level.\n\n**Legacy Setting Support:** This setting is merged with the legacy `python.venvPath` and `python.venvFolders` settings. All paths from these three settings are combined into a single list of search paths. The legacy settings `python.venvPath` and `python.venvFolders` will be deprecated in the future, after which this setting will fully replace them. Please consider migrating your paths to this setting.",
"python-envs.workspaceSearchPaths.description": "Workspace search paths for Python environments. Can be absolute paths or relative directory paths searched within the workspace.",
"python-envs.globalSearchPaths.description": "Absolute paths to search for Python environments across all workspaces. Use for shared environment folders like `~/envs`.",
"python-envs.workspaceSearchPaths.description": "Paths to search for environments in this workspace. By default, searches for a `.venv` folder in the workspace root.",
"python-envs.terminal.revertStartupScriptChanges.title": "Revert Shell Startup Script Changes",
"python-envs.reportIssue.title": "Report Issue",
"python-envs.setEnvManager.title": "Set Environment Manager",
Expand All @@ -31,6 +31,7 @@
"python-envs.setEnvSelected.title": "Set!",
"python-envs.remove.title": "Delete Environment",
"python-envs.refreshAllManagers.title": "Refresh All Environment Managers",
"python-envs.searchSettings.title": "Configure Search Settings",
"python-envs.refreshPackages.title": "Refresh Packages List",
"python-envs.packages.title": "Manage Packages",
"python-envs.clearCache.title": "Clear Cache",
Expand Down
16 changes: 11 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { commands, ExtensionContext, LogOutputChannel, Terminal, Uri, window } from 'vscode';
import { version as extensionVersion } from '../package.json';
import { commands, ExtensionContext, extensions, LogOutputChannel, Terminal, Uri, window } from 'vscode';
import { PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api';
import { ENVS_EXTENSION_ID } from './common/constants';
import { ensureCorrectVersion } from './common/extVersion';
import { registerLogger, traceError, traceInfo, traceVerbose, traceWarn } from './common/logging';
import { registerLogger, traceError, traceInfo, traceWarn } from './common/logging';
import { clearPersistentState, setPersistentState } from './common/persistentState';
import { newProjectSelection } from './common/pickers/managers';
import { StopWatch } from './common/stopWatch';
Expand Down Expand Up @@ -67,6 +67,7 @@ import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInject
import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager';
import { registerTerminalPackageWatcher } from './features/terminal/terminalPackageWatcher';
import { getEnvironmentForTerminal } from './features/terminal/utils';
import { openSearchSettings } from './features/views/envManagerSearch';
import { EnvManagerView } from './features/views/envManagersView';
import { ProjectView } from './features/views/projectView';
import { PythonStatusBarImpl } from './features/views/pythonStatusBar';
Expand Down Expand Up @@ -102,8 +103,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron

ensureCorrectVersion();

// log extension version
traceVerbose(`Python-envs extension version: ${extensionVersion}`);
// Log extension version for diagnostics
const extensionVersion = extensions.getExtension(ENVS_EXTENSION_ID)?.packageJSON?.version;
traceInfo(`Python-envs extension version: ${extensionVersion ?? 'unknown'}`);

// log settings
const configLevels = getEnvManagerAndPackageManagerConfigLevels();
traceInfo(`\n=== ${configLevels.section} ===`);
Expand Down Expand Up @@ -182,6 +185,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
await Promise.all(envManagers.managers.map((m) => m.refresh(undefined)));
});
}),
commands.registerCommand('python-envs.searchSettings', async () => {
await openSearchSettings();
}),
commands.registerCommand('python-envs.refreshPackages', async (item) => {
await refreshPackagesCommand(item, envManagers);
}),
Expand Down
18 changes: 17 additions & 1 deletion src/features/projectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
private readonly updateDebounce = createSimpleDebounce(100, () => this.updateProjects());

initialize(): void {
this.add(this.getInitialProjects());
// Load existing projects from settings without writing back to settings.
// This avoids overwriting user-configured project settings with defaults on reload.
this.loadProjects(this.getInitialProjects());
this.disposables.push(
this._onDidChangeProjects,
new Disposable(() => this._projects.clear()),
Expand Down Expand Up @@ -175,6 +177,20 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
}
}

/**
* Loads projects into the internal map without writing to settings.
* Use this for initial loading from existing settings to avoid overwriting
* user-configured project settings with defaults.
*/
private loadProjects(projects: ProjectArray): void {
projects.forEach((project) => {
this._projects.set(project.uri.toString(), project);
});
if (projects.length > 0) {
this._onDidChangeProjects.fire(Array.from(this._projects.values()));
}
}

create(
name: string,
uri: Uri,
Expand Down
11 changes: 11 additions & 0 deletions src/features/views/envManagerSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { commands } from 'vscode';

/**
* Opens environment search settings at workspace level.
*/
export async function openSearchSettings(): Promise<void> {
await commands.executeCommand(
'workbench.action.openWorkspaceSettings',
'@ext:ms-python.vscode-python-envs "search path"',
);
}
42 changes: 26 additions & 16 deletions src/managers/common/nativePythonFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { PythonProjectApi } from '../../api';
import { spawnProcess } from '../../common/childProcess.apis';
import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../../common/constants';
import { getExtension } from '../../common/extension.apis';
import { traceError, traceLog, traceWarn } from '../../common/logging';
import { traceError, traceVerbose, traceWarn } from '../../common/logging';
import { untildify, untildifyArray } from '../../common/utils/pathUtils';
import { isWindows } from '../../common/utils/platformUtils';
import { createRunningWorkerPool, WorkerPool } from '../../common/utils/workerPool';
Expand Down Expand Up @@ -671,10 +671,23 @@ function getPythonSettingAndUntildify<T>(name: string, scope?: Uri): T | undefin
return value;
}

/**
* Cross-platform check for absolute paths.
* Uses both current platform's check and Windows-specific check to handle
* Windows paths (e.g., C:\path) when running on Unix systems.
*/
function isAbsolutePath(inputPath: string): boolean {
return path.isAbsolute(inputPath) || path.win32.isAbsolute(inputPath);
}

/**
* Gets all extra environment search paths from various configuration sources.
* Combines legacy python settings (with migration), globalSearchPaths, and workspaceSearchPaths.
* @returns Array of search directory paths
*
* Paths can include glob patterns which are expanded by the native
* Python Environment Tool (PET) during environment discovery.
*
* @returns Array of search paths (may include glob patterns)
*/
export async function getAllExtraSearchPaths(): Promise<string[]> {
const searchDirectories: string[] = [];
Expand All @@ -698,7 +711,7 @@ export async function getAllExtraSearchPaths(): Promise<string[]> {

const trimmedPath = searchPath.trim();

if (path.isAbsolute(trimmedPath)) {
if (isAbsolutePath(trimmedPath)) {
// Absolute path - use as is
searchDirectories.push(trimmedPath);
} else {
Expand All @@ -710,20 +723,16 @@ export async function getAllExtraSearchPaths(): Promise<string[]> {
searchDirectories.push(resolvedPath);
}
} else {
traceWarn('Warning: No workspace folders found for relative path:', trimmedPath);
traceWarn('No workspace folders found for relative search path:', trimmedPath);
}
}
}

// Remove duplicates and return
// Remove duplicates and normalize to forward slashes for cross-platform glob compatibility
const uniquePaths = Array.from(new Set(searchDirectories));
traceLog(
'getAllExtraSearchPaths completed. Total unique search directories:',
uniquePaths.length,
'Paths:',
uniquePaths,
);
return uniquePaths;
const normalizedPaths = uniquePaths.map((p) => p.replace(/\\/g, '/'));
traceVerbose('Environment search directories:', normalizedPaths.length, 'paths');
return normalizedPaths;
}

/**
Expand All @@ -745,6 +754,7 @@ function getGlobalSearchPaths(): string[] {

/**
* Gets the most specific workspace-level setting available for workspaceSearchPaths.
* Supports glob patterns which are expanded by PET.
*/
function getWorkspaceSearchPaths(): string[] {
try {
Expand All @@ -753,11 +763,11 @@ function getWorkspaceSearchPaths(): string[] {

if (inspection?.globalValue) {
traceError(
'Error: python-envs.workspaceSearchPaths is set at the user/global level, but this setting can only be set at the workspace or workspace folder level.',
'python-envs.workspaceSearchPaths is set at the user/global level, but this setting can only be set at the workspace or workspace folder level.',
);
}

// For workspace settings, prefer workspaceFolder > workspace
// For workspace settings, prefer workspaceFolder > workspace > default
if (inspection?.workspaceFolderValue) {
return inspection.workspaceFolderValue;
}
Expand All @@ -766,8 +776,8 @@ function getWorkspaceSearchPaths(): string[] {
return inspection.workspaceValue;
}

// Default empty array (don't use global value for workspace settings)
return [];
// Use the default value from package.json
return inspection?.defaultValue ?? [];
} catch (error) {
traceError('Error getting workspaceSearchPaths:', error);
return [];
Expand Down
Loading
Loading