Skip to content

Commit dec6c96

Browse files
Copiloteleanorjboyd
andcommitted
Implement new design: split searchPaths into globalSearchPaths and workspaceSearchPaths
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent fc030f2 commit dec6c96

3 files changed

Lines changed: 150 additions & 78 deletions

File tree

package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,20 @@
110110
"default": false,
111111
"scope": "resource"
112112
},
113-
"python-env.searchPaths": {
113+
"python-env.globalSearchPaths": {
114114
"type": "array",
115-
"description": "%python-env.searchPaths.description%",
115+
"description": "%python-env.globalSearchPaths.description%",
116116
"default": [],
117-
"scope": "window",
117+
"scope": "application",
118+
"items": {
119+
"type": "string"
120+
}
121+
},
122+
"python-env.workspaceSearchPaths": {
123+
"type": "array",
124+
"description": "%python-env.workspaceSearchPaths.description%",
125+
"default": [],
126+
"scope": "resource",
118127
"items": {
119128
"type": "string"
120129
}

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"python-envs.terminal.autoActivationType.shellStartup": "Activation by modifying the terminal shell startup script. To use this feature we will need to modify your shell startup scripts.",
1212
"python-envs.terminal.autoActivationType.off": "No automatic activation of environments.",
1313
"python-envs.terminal.useEnvFile.description": "Controls whether environment variables from .env files and python.envFile setting are injected into terminals.",
14-
"python-env.searchPaths.description": "Additional search paths for Python environments. Can be environment directories or regex patterns (regex patterns are searched within the workspace).",
14+
"python-env.globalSearchPaths.description": "Global search paths for Python environments. Absolute directory paths that are searched at the user level.",
15+
"python-env.workspaceSearchPaths.description": "Workspace search paths for Python environments. Can be relative directory paths or regex patterns searched within the workspace.",
1516
"python-envs.terminal.revertStartupScriptChanges.title": "Revert Shell Startup Script Changes",
1617
"python-envs.reportIssue.title": "Report Issue",
1718
"python-envs.setEnvManager.title": "Set Environment Manager",

src/managers/common/nativePythonFinder.ts

Lines changed: 136 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ function extractEnvironmentDirectory(executablePath: string): string | undefined
408408

409409
/**
410410
* Gets all extra environment search paths from various configuration sources.
411-
* Combines legacy python settings (with migration), python-env.searchPaths settings.
411+
* Combines legacy python settings (with migration), globalSearchPaths, and workspaceSearchPaths.
412412
* @returns Array of search directory paths
413413
*/
414414
async function getAllExtraSearchPaths(): Promise<string[]> {
@@ -419,20 +419,50 @@ async function getAllExtraSearchPaths(): Promise<string[]> {
419419
searchDirectories.push(...customVenvDirs);
420420
traceLog('Added legacy custom venv directories:', customVenvDirs);
421421

422-
// Handle migration from legacy python settings to python-env.searchPaths
422+
// Handle migration from legacy python settings to new search paths settings
423423
await handleLegacyPythonSettingsMigration();
424424

425-
// Get searchPaths using proper VS Code settings precedence
426-
const searchPaths = getSearchPathsWithPrecedence();
427-
traceLog('Retrieved searchPaths with precedence:', searchPaths);
425+
// Get globalSearchPaths (absolute paths, no regex)
426+
const globalSearchPaths = getGlobalSearchPaths();
427+
traceLog('Retrieved globalSearchPaths:', globalSearchPaths);
428+
429+
// Process global search paths (absolute directories only, no regex)
430+
for (const globalPath of globalSearchPaths) {
431+
try {
432+
if (!globalPath || globalPath.trim() === '') {
433+
continue;
434+
}
435+
436+
const trimmedPath = globalPath.trim();
437+
traceLog('Processing global search path:', trimmedPath);
438+
439+
// Global paths must be absolute directories only (no regex patterns)
440+
if (await fs.pathExists(trimmedPath) && (await fs.stat(trimmedPath)).isDirectory()) {
441+
searchDirectories.push(trimmedPath);
442+
traceLog('Added global directory as search path:', trimmedPath);
443+
} else {
444+
// Path doesn't exist yet - might be created later
445+
traceLog('Global path does not exist currently, adding for future resolution:', trimmedPath);
446+
searchDirectories.push(trimmedPath);
447+
}
448+
} catch (error) {
449+
traceLog('Error processing global search path:', globalPath, 'Error:', error);
450+
}
451+
}
452+
453+
// Get workspaceSearchPaths (can include regex patterns)
454+
const workspaceSearchPaths = getWorkspaceSearchPaths();
455+
traceLog('Retrieved workspaceSearchPaths:', workspaceSearchPaths);
428456

429-
for (const searchPath of searchPaths) {
457+
// Process workspace search paths (can be directories or regex patterns)
458+
for (const searchPath of workspaceSearchPaths) {
430459
try {
431460
if (!searchPath || searchPath.trim() === '') {
432461
continue;
433462
}
434463

435464
const trimmedPath = searchPath.trim();
465+
traceLog('Processing workspace search path:', trimmedPath);
436466

437467
// Check if it's a regex pattern (contains regex special characters)
438468
// Note: Windows paths contain backslashes, so we need to be more careful
@@ -443,7 +473,7 @@ async function getAllExtraSearchPaths(): Promise<string[]> {
443473

444474
if (isRegexPattern) {
445475
traceLog('Processing regex pattern for Python environment discovery:', trimmedPath);
446-
traceLog('Warning: Using regex patterns in searchPaths may cause performance issues due to file system scanning');
476+
traceLog('Warning: Using regex patterns in workspaceSearchPaths may cause performance issues due to file system scanning');
447477

448478
// Use workspace.findFiles to search with the regex pattern as literally as possible
449479
const foundFiles = await workspace.findFiles(trimmedPath, null);
@@ -461,21 +491,21 @@ async function getAllExtraSearchPaths(): Promise<string[]> {
461491
}
462492
}
463493

464-
traceLog('Completed processing regex pattern:', trimmedPath, 'Added', searchDirectories.length, 'search directories');
494+
traceLog('Completed processing regex pattern:', trimmedPath);
465495
}
466496
// Check if it's a directory path
467497
else if (await fs.pathExists(trimmedPath) && (await fs.stat(trimmedPath)).isDirectory()) {
468-
traceLog('Processing directory path:', trimmedPath);
498+
traceLog('Processing workspace directory path:', trimmedPath);
469499
searchDirectories.push(trimmedPath);
470-
traceLog('Added directory as search path:', trimmedPath);
500+
traceLog('Added workspace directory as search path:', trimmedPath);
471501
}
472-
// Path doesn't exist yet - might be created later (virtual envs, network drives, symlinks)
502+
// Path doesn't exist yet - might be created later
473503
else {
474-
traceLog('Path does not exist currently, adding for future resolution:', trimmedPath);
504+
traceLog('Workspace path does not exist currently, adding for future resolution:', trimmedPath);
475505
searchDirectories.push(trimmedPath);
476506
}
477507
} catch (error) {
478-
traceLog('Error processing search path:', searchPath, 'Error:', error);
508+
traceLog('Error processing workspace search path:', searchPath, 'Error:', error);
479509
}
480510
}
481511

@@ -486,38 +516,48 @@ async function getAllExtraSearchPaths(): Promise<string[]> {
486516
}
487517

488518
/**
489-
* Gets searchPaths setting value using proper VS Code settings precedence.
490-
* Checks workspaceFolder, then workspace, then user level settings.
491-
* @returns Array of search paths from the most specific scope available
519+
* Gets globalSearchPaths setting with proper validation.
520+
* Only gets user-level (global) setting since this setting is application-scoped.
492521
*/
493-
function getSearchPathsWithPrecedence(): string[] {
522+
function getGlobalSearchPaths(): string[] {
494523
try {
495-
// Use VS Code configuration inspection to handle precedence automatically
496-
const config = getConfiguration('python-env');
497-
const inspection = config.inspect<string[]>('searchPaths');
524+
const envConfig = getConfiguration('python-env');
525+
const inspection = envConfig.inspect<string[]>('globalSearchPaths');
498526

499-
// VS Code automatically handles precedence: workspaceFolder -> workspace -> user
500-
// We check each level in order and return the first one found
527+
const globalPaths = inspection?.globalValue || [];
528+
traceLog('Retrieved globalSearchPaths:', globalPaths);
529+
return untildifyArray(globalPaths);
530+
} catch (error) {
531+
traceLog('Error getting globalSearchPaths:', error);
532+
return [];
533+
}
534+
}
535+
536+
/**
537+
* Gets workspaceSearchPaths setting with workspace precedence.
538+
* Gets the most specific workspace-level setting available.
539+
*/
540+
function getWorkspaceSearchPaths(): string[] {
541+
try {
542+
const envConfig = getConfiguration('python-env');
543+
const inspection = envConfig.inspect<string[]>('workspaceSearchPaths');
544+
545+
// For workspace settings, prefer workspaceFolder > workspace
501546
if (inspection?.workspaceFolderValue) {
502-
traceLog('Using workspaceFolder level searchPaths setting');
503-
return untildifyArray(inspection.workspaceFolderValue);
547+
traceLog('Using workspaceFolder level workspaceSearchPaths setting');
548+
return inspection.workspaceFolderValue;
504549
}
505550

506551
if (inspection?.workspaceValue) {
507-
traceLog('Using workspace level searchPaths setting');
508-
return untildifyArray(inspection.workspaceValue);
509-
}
510-
511-
if (inspection?.globalValue) {
512-
traceLog('Using user level searchPaths setting');
513-
return untildifyArray(inspection.globalValue);
552+
traceLog('Using workspace level workspaceSearchPaths setting');
553+
return inspection.workspaceValue;
514554
}
515555

516-
// Default empty array
517-
traceLog('No searchPaths setting found at any level, using empty array');
556+
// Default empty array (don't use global value for workspace settings)
557+
traceLog('No workspaceSearchPaths setting found at workspace level, using empty array');
518558
return [];
519559
} catch (error) {
520-
traceLog('Error getting searchPaths with precedence:', error);
560+
traceLog('Error getting workspaceSearchPaths:', error);
521561
return [];
522562
}
523563
}
@@ -532,68 +572,90 @@ function untildifyArray(paths: string[]): string[] {
532572
}
533573

534574
/**
535-
* Handles migration from legacy python settings (python.venvPath and python.venvFolders) to python-env.searchPaths.
536-
* Only migrates if legacy settings exist and searchPaths is different.
575+
* Handles migration from legacy python settings to the new globalSearchPaths and workspaceSearchPaths settings.
576+
* Legacy global settings go to globalSearchPaths, workspace settings go to workspaceSearchPaths.
577+
* Does NOT delete the old settings, only adds them to the new settings.
537578
*/
538579
async function handleLegacyPythonSettingsMigration(): Promise<void> {
539580
try {
540581
const pythonConfig = getConfiguration('python');
541582
const envConfig = getConfiguration('python-env');
542583

543-
// Get legacy settings
584+
// Get legacy settings at all levels
544585
const venvPathInspection = pythonConfig.inspect<string>('venvPath');
545-
const venvPath = venvPathInspection?.globalValue;
546-
547586
const venvFoldersInspection = pythonConfig.inspect<string[]>('venvFolders');
548-
const venvFolders = venvFoldersInspection?.globalValue || [];
549587

550-
// Collect all legacy paths
551-
const legacyPaths: string[] = [];
552-
if (venvPath) {
553-
legacyPaths.push(venvPath);
588+
// Collect global (user-level) legacy paths for globalSearchPaths
589+
const globalLegacyPaths: string[] = [];
590+
if (venvPathInspection?.globalValue) {
591+
globalLegacyPaths.push(venvPathInspection.globalValue);
554592
}
555-
legacyPaths.push(...venvFolders);
556-
557-
if (legacyPaths.length === 0) {
558-
return;
593+
if (venvFoldersInspection?.globalValue) {
594+
globalLegacyPaths.push(...venvFoldersInspection.globalValue);
559595
}
560596

561-
traceLog('Found legacy python settings - venvPath:', venvPath, 'venvFolders:', venvFolders);
562-
563-
// Check current searchPaths at user level
564-
const searchPathsInspection = envConfig.inspect<string[]>('searchPaths');
565-
const currentSearchPaths = searchPathsInspection?.globalValue || [];
597+
// Collect workspace-level legacy paths for workspaceSearchPaths
598+
const workspaceLegacyPaths: string[] = [];
599+
if (venvPathInspection?.workspaceValue) {
600+
workspaceLegacyPaths.push(venvPathInspection.workspaceValue);
601+
}
602+
if (venvFoldersInspection?.workspaceValue) {
603+
workspaceLegacyPaths.push(...venvFoldersInspection.workspaceValue);
604+
}
605+
if (venvPathInspection?.workspaceFolderValue) {
606+
workspaceLegacyPaths.push(venvPathInspection.workspaceFolderValue);
607+
}
608+
if (venvFoldersInspection?.workspaceFolderValue) {
609+
workspaceLegacyPaths.push(...venvFoldersInspection.workspaceFolderValue);
610+
}
566611

567-
// Check if they are the same (no need to migrate)
568-
if (arraysEqual(legacyPaths, currentSearchPaths)) {
569-
traceLog('Legacy settings and searchPaths are identical, no migration needed');
612+
if (globalLegacyPaths.length === 0 && workspaceLegacyPaths.length === 0) {
570613
return;
571614
}
572615

573-
// Combine legacy paths with existing searchPaths (remove duplicates)
574-
const combinedPaths = Array.from(new Set([...currentSearchPaths, ...legacyPaths]));
575-
576-
// Update searchPaths at user level
577-
await envConfig.update('searchPaths', combinedPaths, true); // true = global/user level
616+
traceLog('Found legacy python settings - global paths:', globalLegacyPaths, 'workspace paths:', workspaceLegacyPaths);
578617

579-
// Delete the old legacy settings
580-
if (venvPath) {
581-
await pythonConfig.update('venvPath', undefined, true);
582-
}
583-
if (venvFolders.length > 0) {
584-
await pythonConfig.update('venvFolders', undefined, true);
618+
// Migrate global legacy paths to globalSearchPaths
619+
if (globalLegacyPaths.length > 0) {
620+
const globalSearchPathsInspection = envConfig.inspect<string[]>('globalSearchPaths');
621+
const currentGlobalSearchPaths = globalSearchPathsInspection?.globalValue || [];
622+
623+
// Only migrate if they are different
624+
if (!arraysEqual(globalLegacyPaths, currentGlobalSearchPaths)) {
625+
const combinedGlobalPaths = Array.from(new Set([...currentGlobalSearchPaths, ...globalLegacyPaths]));
626+
await envConfig.update('globalSearchPaths', combinedGlobalPaths, true); // true = global/user level
627+
traceLog('Migrated legacy global python settings to globalSearchPaths. Combined paths:', combinedGlobalPaths);
628+
} else {
629+
traceLog('Legacy global settings and globalSearchPaths are identical, no migration needed');
630+
}
585631
}
586632

587-
traceLog('Migrated legacy python settings to searchPaths and removed old settings. Combined paths:', combinedPaths);
633+
// Migrate workspace legacy paths to workspaceSearchPaths
634+
if (workspaceLegacyPaths.length > 0) {
635+
const workspaceSearchPathsInspection = envConfig.inspect<string[]>('workspaceSearchPaths');
636+
const currentWorkspaceSearchPaths = workspaceSearchPathsInspection?.workspaceValue || [];
637+
638+
// Only migrate if they are different
639+
if (!arraysEqual(workspaceLegacyPaths, currentWorkspaceSearchPaths)) {
640+
const combinedWorkspacePaths = Array.from(new Set([...currentWorkspaceSearchPaths, ...workspaceLegacyPaths]));
641+
await envConfig.update('workspaceSearchPaths', combinedWorkspacePaths, false); // false = workspace level
642+
traceLog('Migrated legacy workspace python settings to workspaceSearchPaths. Combined paths:', combinedWorkspacePaths);
643+
} else {
644+
traceLog('Legacy workspace settings and workspaceSearchPaths are identical, no migration needed');
645+
}
646+
}
588647

589648
// Show notification to user about migration
590-
// Note: We should only show this once per session to avoid spam
591-
if (!migrationNotificationShown) {
649+
if (!migrationNotificationShown && (globalLegacyPaths.length > 0 || workspaceLegacyPaths.length > 0)) {
592650
migrationNotificationShown = true;
593-
// Note: Actual notification would use VS Code's window.showInformationMessage
594-
// but we'll log it for now since we can't import window APIs here
595-
const settingsRemoved = [venvPath ? 'python.venvPath' : '', venvFolders.length > 0 ? 'python.venvFolders' : ''].filter(Boolean).join(' and ');
596-
traceLog(`User notification: Automatically migrated ${settingsRemoved} to python-env.searchPaths and removed the old settings.`);
651+
const migratedSettings = [];
652+
if (globalLegacyPaths.length > 0) {
653+
migratedSettings.push('legacy global settings to python-env.globalSearchPaths');
654+
}
655+
if (workspaceLegacyPaths.length > 0) {
656+
migratedSettings.push('legacy workspace settings to python-env.workspaceSearchPaths');
657+
}
658+
traceLog(`User notification: Automatically migrated ${migratedSettings.join(' and ')}.`);
597659
}
598660
} catch (error) {
599661
traceLog('Error during legacy python settings migration:', error);

0 commit comments

Comments
 (0)