@@ -204,57 +204,27 @@ pub fn handle_refresh(context: Arc<Context>, id: u32, params: Value) {
204204 // Ensure we can have only one refresh at a time.
205205 let lock = REFRESH_LOCK . lock ( ) . expect ( "REFRESH_LOCK mutex poisoned" ) ;
206206
207- let mut config = context. configuration . read ( ) . unwrap ( ) . clone ( ) ;
207+ let config = context. configuration . read ( ) . unwrap ( ) . clone ( ) ;
208208 let reporter = Arc :: new ( CacheReporter :: new ( Arc :: new ( jsonrpc:: create_reporter (
209209 refresh_options. search_kind ,
210210 ) ) ) ) ;
211211
212- let mut search_scope = None ;
213-
214- // If search kind is provided and no search_paths, then we will only search in the global locations.
215- if refresh_options. search_kind . is_some ( ) || refresh_options. search_paths . is_some ( ) {
216- // Always clear this, as we will either serach in specified folder or a specific kind in global locations.
217- config. workspace_directories = None ;
218- if let Some ( search_paths) = refresh_options. search_paths {
219- // Expand any glob patterns in the search paths
220- let expanded_paths = expand_glob_patterns ( & search_paths) ;
221- trace ! (
222- "Expanded {} search paths to {} paths" ,
223- search_paths. len( ) ,
224- expanded_paths. len( )
225- ) ;
226- // These workspace folders are only for this refresh.
227- config. workspace_directories = Some (
228- expanded_paths
229- . iter ( )
230- . filter ( |p| p. is_dir ( ) )
231- . cloned ( )
232- . collect ( ) ,
233- ) ;
234- config. executables = Some (
235- expanded_paths
236- . iter ( )
237- . filter ( |p| p. is_file ( ) )
238- . cloned ( )
239- . collect ( ) ,
240- ) ;
241- search_scope = Some ( SearchScope :: Workspace ) ;
242- } else if let Some ( search_kind) = refresh_options. search_kind {
243- config. executables = None ;
244- search_scope = Some ( SearchScope :: Global ( search_kind) ) ;
245- }
212+ let ( config, search_scope) = build_refresh_config ( & refresh_options, config) ;
213+ if refresh_options. search_paths . is_some ( ) {
214+ trace ! (
215+ "Expanded search paths to {} workspace dirs, {} executables" ,
216+ config
217+ . workspace_directories
218+ . as_ref( )
219+ . map( |v| v. len( ) )
220+ . unwrap_or( 0 ) ,
221+ config. executables. as_ref( ) . map( |v| v. len( ) ) . unwrap_or( 0 )
222+ ) ;
223+ }
246224
247- // Configure the locators with the modified config.
248- for locator in context. locators . iter ( ) {
249- locator. configure ( & config) ;
250- }
251- } else {
252- // Re-configure the locators with an un-modified config.
253- // Possible we congirued the locators with a modified config in the in the previous request.
254- // & the config was scoped to a particular search folder, executables or kind.
255- for locator in context. locators . iter ( ) {
256- locator. configure ( & config) ;
257- }
225+ // Configure the locators with the (possibly modified) config.
226+ for locator in context. locators . iter ( ) {
227+ locator. configure ( & config) ;
258228 }
259229
260230 trace ! ( "Start refreshing environments, config: {:?}" , config) ;
@@ -497,3 +467,155 @@ pub fn handle_clear_cache(_context: Arc<Context>, id: u32, _params: Value) {
497467 }
498468 } ) ;
499469}
470+
471+ /// Builds the configuration and search scope based on refresh options.
472+ /// This is extracted from handle_refresh to enable unit testing.
473+ ///
474+ /// Returns (modified_config, search_scope)
475+ pub ( crate ) fn build_refresh_config (
476+ refresh_options : & RefreshOptions ,
477+ mut config : Configuration ,
478+ ) -> ( Configuration , Option < SearchScope > ) {
479+ let mut search_scope = None ;
480+
481+ // If search_paths is provided, limit search to those paths.
482+ // If only search_kind is provided (without search_paths), we still search
483+ // workspace directories because many environment types (like Venv, VirtualEnv)
484+ // don't have global locations - they only exist in workspace folders.
485+ // The reporter will filter results to only report the requested kind.
486+ if let Some ( ref search_paths) = refresh_options. search_paths {
487+ // Clear workspace directories when explicit search paths are provided.
488+ config. workspace_directories = None ;
489+ // Expand any glob patterns in the search paths
490+ let expanded_paths = expand_glob_patterns ( search_paths) ;
491+ // These workspace folders are only for this refresh.
492+ config. workspace_directories = Some (
493+ expanded_paths
494+ . iter ( )
495+ . filter ( |p| p. is_dir ( ) )
496+ . cloned ( )
497+ . collect ( ) ,
498+ ) ;
499+ config. executables = Some (
500+ expanded_paths
501+ . iter ( )
502+ . filter ( |p| p. is_file ( ) )
503+ . cloned ( )
504+ . collect ( ) ,
505+ ) ;
506+ search_scope = Some ( SearchScope :: Workspace ) ;
507+ } else if let Some ( search_kind) = refresh_options. search_kind {
508+ // When only search_kind is provided, keep workspace directories so that
509+ // workspace-based environments (Venv, VirtualEnv, etc.) can be found.
510+ // The search_scope tells find_and_report_envs to filter locators by kind.
511+ search_scope = Some ( SearchScope :: Global ( search_kind) ) ;
512+ }
513+
514+ ( config, search_scope)
515+ }
516+
517+ #[ cfg( test) ]
518+ mod tests {
519+ use super :: * ;
520+ use std:: path:: PathBuf ;
521+
522+ /// Test for https://github.com/microsoft/python-environment-tools/issues/151
523+ /// Verifies that when searchKind is provided (without searchPaths),
524+ /// workspace_directories are NOT cleared.
525+ ///
526+ /// The bug was that handle_refresh cleared workspace_directories when searchKind
527+ /// was provided, preventing discovery of workspace-based environments like venvs.
528+ #[ test]
529+ fn test_search_kind_preserves_workspace_directories ( ) {
530+ let workspace = PathBuf :: from ( "/test/workspace" ) ;
531+ let config = Configuration {
532+ workspace_directories : Some ( vec ! [ workspace. clone( ) ] ) ,
533+ ..Default :: default ( )
534+ } ;
535+
536+ let refresh_options = RefreshOptions {
537+ search_kind : Some ( PythonEnvironmentKind :: Venv ) ,
538+ search_paths : None ,
539+ } ;
540+
541+ let ( result_config, search_scope) = build_refresh_config ( & refresh_options, config) ;
542+
543+ // CRITICAL: workspace_directories must be preserved when only search_kind is provided
544+ assert_eq ! (
545+ result_config. workspace_directories,
546+ Some ( vec![ workspace] ) ,
547+ "workspace_directories should NOT be cleared when only searchKind is provided"
548+ ) ;
549+
550+ // search_scope should be Global with the requested kind
551+ assert ! (
552+ matches!(
553+ search_scope,
554+ Some ( SearchScope :: Global ( PythonEnvironmentKind :: Venv ) )
555+ ) ,
556+ "search_scope should be Global(Venv)"
557+ ) ;
558+ }
559+
560+ /// Test that when searchPaths is provided, workspace_directories ARE replaced.
561+ #[ test]
562+ fn test_search_paths_replaces_workspace_directories ( ) {
563+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
564+ let search_dir = temp_dir. path ( ) . join ( "search_path" ) ;
565+ std:: fs:: create_dir ( & search_dir) . unwrap ( ) ;
566+
567+ let original_workspace = PathBuf :: from ( "/original/workspace" ) ;
568+ let config = Configuration {
569+ workspace_directories : Some ( vec ! [ original_workspace] ) ,
570+ ..Default :: default ( )
571+ } ;
572+
573+ let refresh_options = RefreshOptions {
574+ search_kind : None ,
575+ search_paths : Some ( vec ! [ search_dir. clone( ) ] ) ,
576+ } ;
577+
578+ let ( result_config, search_scope) = build_refresh_config ( & refresh_options, config) ;
579+
580+ // workspace_directories should be replaced with the search_paths directory
581+ assert_eq ! (
582+ result_config. workspace_directories,
583+ Some ( vec![ search_dir] ) ,
584+ "workspace_directories should be replaced by search_paths"
585+ ) ;
586+
587+ assert ! (
588+ matches!( search_scope, Some ( SearchScope :: Workspace ) ) ,
589+ "search_scope should be Workspace"
590+ ) ;
591+ }
592+
593+ /// Test that when neither searchKind nor searchPaths is provided,
594+ /// configuration is unchanged.
595+ #[ test]
596+ fn test_no_options_preserves_config ( ) {
597+ let workspace = PathBuf :: from ( "/test/workspace" ) ;
598+ let config = Configuration {
599+ workspace_directories : Some ( vec ! [ workspace. clone( ) ] ) ,
600+ ..Default :: default ( )
601+ } ;
602+
603+ let refresh_options = RefreshOptions {
604+ search_kind : None ,
605+ search_paths : None ,
606+ } ;
607+
608+ let ( result_config, search_scope) = build_refresh_config ( & refresh_options, config) ;
609+
610+ assert_eq ! (
611+ result_config. workspace_directories,
612+ Some ( vec![ workspace] ) ,
613+ "workspace_directories should be preserved when no options provided"
614+ ) ;
615+
616+ assert ! (
617+ search_scope. is_none( ) ,
618+ "search_scope should be None when no options provided"
619+ ) ;
620+ }
621+ }
0 commit comments