@@ -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 */
414414async 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 */
538579async 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