@@ -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+
484618export function getCacheDirectory ( context : ExtensionContext ) : Uri {
485619 return Uri . joinPath ( context . globalStorageUri , 'pythonLocator' ) ;
486620}
0 commit comments