Skip to content

Commit c885a3b

Browse files
Copiloteleanorjboyd
andcommitted
Refactor searchPaths implementation per feedback - remove direct executable paths support, improve logging, refactor configure function
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent a34b517 commit c885a3b

4 files changed

Lines changed: 257 additions & 80 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"type": "array",
115115
"description": "%python-env.searchPaths.description%",
116116
"default": [],
117-
"scope": "resource",
117+
"scope": "window",
118118
"items": {
119119
"type": "string"
120120
}

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
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 direct executable paths (/bin/python), environment directories, or regex patterns (efficiency warning applies to regex).",
14+
"python-env.searchPaths.description": "Additional search paths for Python environments. Can be environment directories or regex patterns (efficiency warning applies to regex).",
1515
"python-envs.terminal.revertStartupScriptChanges.title": "Revert Shell Startup Script Changes",
1616
"python-envs.reportIssue.title": "Report Issue",
1717
"python-envs.setEnvManager.title": "Set Environment Manager",

src/managers/common/nativePythonFinder.ts

Lines changed: 181 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -326,23 +326,22 @@ class NativePythonFinderImpl implements NativePythonFinder {
326326
* Must be invoked when ever there are changes to any data related to the configuration details.
327327
*/
328328
private async configure() {
329-
// Get custom virtual environment directories
329+
// Get custom virtual environment directories from legacy python settings
330330
const customVenvDirs = getCustomVirtualEnvDirs();
331331

332-
// Get final search set from searchPaths setting
333-
const finalSearchSet = await createFinalSearchSet();
332+
// Get additional search paths from the new searchPaths setting
333+
const extraSearchPaths = await getAllExtraSearchPaths();
334334

335335
// Combine and deduplicate all environment directories
336-
const allEnvironmentDirectories = [...customVenvDirs, ...finalSearchSet];
336+
const allEnvironmentDirectories = [...customVenvDirs, ...extraSearchPaths];
337337
const environmentDirectories = Array.from(new Set(allEnvironmentDirectories));
338338

339-
traceLog('Custom venv directories:', customVenvDirs);
340-
traceLog('Final search set:', finalSearchSet);
341-
traceLog('Combined environment directories:', environmentDirectories);
339+
traceLog('Legacy custom venv directories:', customVenvDirs);
340+
traceLog('Extra search paths from settings:', extraSearchPaths);
341+
traceLog('Final combined environment directories:', environmentDirectories);
342342

343343
const options: ConfigurationOptions = {
344344
workspaceDirectories: this.api.getPythonProjects().map((item) => item.uri.fsPath),
345-
// Include both custom venv dirs and search paths
346345
environmentDirectories,
347346
condaExecutable: getPythonSettingAndUntildify<string>('condaPath'),
348347
poetryExecutable: getPythonSettingAndUntildify<string>('poetryPath'),
@@ -394,23 +393,42 @@ function getPythonSettingAndUntildify<T>(name: string, scope?: Uri): T | undefin
394393
return value;
395394
}
396395

397-
function getPythonEnvSettingAndUntildify<T>(name: string, scope?: Uri): T | undefined {
398-
const value = getConfiguration('python-env', scope).get<T>(name);
399-
if (typeof value === 'string') {
400-
return value ? (untildify(value as string) as unknown as T) : undefined;
396+
/**
397+
* Extracts the great-grandparent directory from a Python executable path.
398+
* This follows the pattern: executable -> bin -> env -> search directory
399+
* @param executablePath Path to Python executable
400+
* @returns The great-grandparent directory path, or undefined if not found
401+
*/
402+
function extractGreatGrandparentDirectory(executablePath: string): string | undefined {
403+
try {
404+
const grandGrandParent = path.dirname(path.dirname(path.dirname(executablePath)));
405+
if (grandGrandParent && grandGrandParent !== path.dirname(grandGrandParent)) {
406+
traceLog('Extracted great-grandparent directory:', grandGrandParent, 'from executable:', executablePath);
407+
return grandGrandParent;
408+
} else {
409+
traceLog('Warning: Could not find valid great-grandparent directory for executable:', executablePath);
410+
return undefined;
411+
}
412+
} catch (error) {
413+
traceLog('Error extracting great-grandparent directory from:', executablePath, 'Error:', error);
414+
return undefined;
401415
}
402-
return value;
403416
}
404417

405418
/**
406-
* Creates the final search set from configured search paths.
407-
* Handles executables, directories, and regex patterns.
419+
* Gets all extra environment search paths from various configuration sources.
420+
* Combines python.venvFolders (for migration) and python-env.searchPaths settings.
421+
* @returns Array of search directory paths
408422
*/
409-
async function createFinalSearchSet(): Promise<string[]> {
410-
const searchPaths = getPythonEnvSettingAndUntildify<string[]>('searchPaths') ?? [];
411-
const finalSearchSet: string[] = [];
412-
413-
traceLog('Processing search paths:', searchPaths);
423+
async function getAllExtraSearchPaths(): Promise<string[]> {
424+
const searchDirectories: string[] = [];
425+
426+
// Handle migration from python.venvFolders to python-env.searchPaths
427+
await handleVenvFoldersMigration();
428+
429+
// Get searchPaths using proper VS Code settings precedence
430+
const searchPaths = getSearchPathsWithPrecedence();
431+
traceLog('Retrieved searchPaths with precedence:', searchPaths);
414432

415433
for (const searchPath of searchPaths) {
416434
try {
@@ -428,59 +446,175 @@ async function createFinalSearchSet(): Promise<string[]> {
428446
const isRegexPattern = regexChars.test(trimmedPath) || (hasBackslash && !isWindowsPath);
429447

430448
if (isRegexPattern) {
431-
traceLog('Processing regex pattern:', trimmedPath);
432-
traceLog('Warning: Using regex patterns in searchPaths may cause performance issues');
449+
traceLog('Processing regex pattern for Python environment discovery:', trimmedPath);
450+
traceLog('Warning: Using regex patterns in searchPaths may cause performance issues due to file system scanning');
433451

434452
// Use workspace.findFiles to search for python executables
435453
const foundFiles = await workspace.findFiles(trimmedPath, null);
454+
traceLog('Regex pattern search found', foundFiles.length, 'files matching pattern:', trimmedPath);
436455

437456
for (const file of foundFiles) {
438457
const filePath = file.fsPath;
458+
traceLog('Evaluating file from regex search:', filePath);
459+
439460
// Check if it's likely a python executable
440461
if (filePath.toLowerCase().includes('python') || path.basename(filePath).startsWith('python')) {
441-
// Get grand-grand parent folder (file -> bin -> env -> this)
442-
const grandGrandParent = path.dirname(path.dirname(path.dirname(filePath)));
443-
if (grandGrandParent && grandGrandParent !== path.dirname(grandGrandParent)) {
444-
finalSearchSet.push(grandGrandParent);
445-
traceLog('Added grand-grand parent from regex match:', grandGrandParent);
462+
traceLog('File appears to be a Python executable:', filePath);
463+
464+
// Get great-grandparent folder (file -> bin -> env -> this)
465+
const greatGrandParent = extractGreatGrandparentDirectory(filePath);
466+
if (greatGrandParent) {
467+
searchDirectories.push(greatGrandParent);
468+
traceLog('Added search directory from regex match:', greatGrandParent);
446469
}
470+
} else {
471+
traceLog('File does not appear to be a Python executable, skipping:', filePath);
447472
}
448473
}
449-
}
450-
// Check if it's a direct executable path
451-
else if (await fs.pathExists(trimmedPath) && (await fs.stat(trimmedPath)).isFile()) {
452-
traceLog('Processing executable path:', trimmedPath);
453-
// Get grand-grand parent folder
454-
const grandGrandParent = path.dirname(path.dirname(path.dirname(trimmedPath)));
455-
if (grandGrandParent && grandGrandParent !== path.dirname(grandGrandParent)) {
456-
finalSearchSet.push(grandGrandParent);
457-
traceLog('Added grand-grand parent from executable:', grandGrandParent);
458-
}
474+
475+
traceLog('Completed processing regex pattern:', trimmedPath, 'Added', searchDirectories.length, 'search directories');
459476
}
460477
// Check if it's a directory path
461478
else if (await fs.pathExists(trimmedPath) && (await fs.stat(trimmedPath)).isDirectory()) {
462479
traceLog('Processing directory path:', trimmedPath);
463-
// Add directory as-is
464-
finalSearchSet.push(trimmedPath);
465-
traceLog('Added directory as-is:', trimmedPath);
480+
searchDirectories.push(trimmedPath);
481+
traceLog('Added directory as search path:', trimmedPath);
466482
}
467-
// If path doesn't exist, try to check if it could be an executable that might exist later
483+
// If path doesn't exist, try to check if it could be a directory that might exist later
468484
else {
469-
traceLog('Path does not exist, treating as potential directory:', trimmedPath);
470-
// Treat as directory path for future resolution
471-
finalSearchSet.push(trimmedPath);
485+
traceLog('Path does not exist currently, treating as potential directory for future resolution:', trimmedPath);
486+
searchDirectories.push(trimmedPath);
472487
}
473488
} catch (error) {
474-
traceLog('Error processing search path:', searchPath, error);
489+
traceLog('Error processing search path:', searchPath, 'Error:', error);
475490
}
476491
}
477492

478493
// Remove duplicates and return
479-
const uniquePaths = Array.from(new Set(finalSearchSet));
480-
traceLog('Final search set created:', uniquePaths);
494+
const uniquePaths = Array.from(new Set(searchDirectories));
495+
traceLog('getAllExtraSearchPaths completed. Total unique search directories:', uniquePaths.length, 'Paths:', uniquePaths);
481496
return uniquePaths;
482497
}
483498

499+
/**
500+
* Gets searchPaths setting value using proper VS Code settings precedence.
501+
* Checks workspaceFolder, then workspace, then user level settings.
502+
* @returns Array of search paths from the most specific scope available
503+
*/
504+
function getSearchPathsWithPrecedence(): string[] {
505+
try {
506+
// Get the workspace folders for proper precedence checking
507+
const workspaceFolders = workspace.workspaceFolders;
508+
509+
// Try workspaceFolder level first (most specific)
510+
if (workspaceFolders && workspaceFolders.length > 0) {
511+
for (const folder of workspaceFolders) {
512+
const config = getConfiguration('python-env', folder.uri);
513+
const inspection = config.inspect<string[]>('searchPaths');
514+
515+
if (inspection?.workspaceFolderValue) {
516+
traceLog('Using workspaceFolder level searchPaths setting from:', folder.uri.fsPath);
517+
return untildifyArray(inspection.workspaceFolderValue);
518+
}
519+
}
520+
}
521+
522+
// Try workspace level next
523+
const config = getConfiguration('python-env');
524+
const inspection = config.inspect<string[]>('searchPaths');
525+
526+
if (inspection?.workspaceValue) {
527+
traceLog('Using workspace level searchPaths setting');
528+
return untildifyArray(inspection.workspaceValue);
529+
}
530+
531+
// Fall back to user level
532+
if (inspection?.globalValue) {
533+
traceLog('Using user level searchPaths setting');
534+
return untildifyArray(inspection.globalValue);
535+
}
536+
537+
// Default empty array
538+
traceLog('No searchPaths setting found at any level, using empty array');
539+
return [];
540+
} catch (error) {
541+
traceLog('Error getting searchPaths with precedence:', error);
542+
return [];
543+
}
544+
}
545+
546+
/**
547+
* Applies untildify to an array of paths
548+
* @param paths Array of potentially tilde-containing paths
549+
* @returns Array of expanded paths
550+
*/
551+
function untildifyArray(paths: string[]): string[] {
552+
return paths.map(p => untildify(p));
553+
}
554+
555+
/**
556+
* Handles migration from python.venvFolders to python-env.searchPaths.
557+
* Only migrates if python.venvFolders exists and searchPaths is different.
558+
*/
559+
async function handleVenvFoldersMigration(): Promise<void> {
560+
try {
561+
// Check if we have python.venvFolders set at user level
562+
const pythonConfig = getConfiguration('python');
563+
const venvFoldersInspection = pythonConfig.inspect<string[]>('venvFolders');
564+
const venvFolders = venvFoldersInspection?.globalValue;
565+
566+
if (!venvFolders || venvFolders.length === 0) {
567+
traceLog('No python.venvFolders setting found, skipping migration');
568+
return;
569+
}
570+
571+
traceLog('Found python.venvFolders setting:', venvFolders);
572+
573+
// Check current searchPaths at user level
574+
const envConfig = getConfiguration('python-env');
575+
const searchPathsInspection = envConfig.inspect<string[]>('searchPaths');
576+
const currentSearchPaths = searchPathsInspection?.globalValue || [];
577+
578+
// Check if they are the same (no need to migrate)
579+
if (arraysEqual(venvFolders, currentSearchPaths)) {
580+
traceLog('python.venvFolders and searchPaths are identical, no migration needed');
581+
return;
582+
}
583+
584+
// Combine venvFolders with existing searchPaths (remove duplicates)
585+
const combinedPaths = Array.from(new Set([...currentSearchPaths, ...venvFolders]));
586+
587+
// Update searchPaths at user level
588+
await envConfig.update('searchPaths', combinedPaths, true); // true = global/user level
589+
590+
traceLog('Migrated python.venvFolders to searchPaths. Combined paths:', combinedPaths);
591+
592+
// Show notification to user about migration
593+
// Note: We should only show this once per session to avoid spam
594+
if (!migrationNotificationShown) {
595+
migrationNotificationShown = true;
596+
// Note: Actual notification would use VS Code's window.showInformationMessage
597+
// but we'll log it for now since we can't import window APIs here
598+
traceLog('User notification: Automatically migrated python.venvFolders setting to python-env.searchPaths');
599+
}
600+
} catch (error) {
601+
traceLog('Error during venvFolders migration:', error);
602+
}
603+
}
604+
605+
/**
606+
* Helper function to compare two arrays for equality
607+
*/
608+
function arraysEqual<T>(a: T[], b: T[]): boolean {
609+
if (a.length !== b.length) {
610+
return false;
611+
}
612+
return a.every((val, index) => val === b[index]);
613+
}
614+
615+
// Module-level variable to track migration notification
616+
let migrationNotificationShown = false;
617+
484618
export function getCacheDirectory(context: ExtensionContext): Uri {
485619
return Uri.joinPath(context.globalStorageUri, 'pythonLocator');
486620
}

0 commit comments

Comments
 (0)