@@ -204,66 +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_paths is provided, limit search to those paths.
215- // If only search_kind is provided (without search_paths), we still search
216- // workspace directories because many environment types (like Venv, VirtualEnv)
217- // don't have global locations - they only exist in workspace folders.
218- // The reporter will filter results to only report the requested kind.
219- if let Some ( search_paths) = refresh_options. search_paths {
220- // Clear workspace directories when explicit search paths are provided.
221- config. workspace_directories = None ;
222- // Expand any glob patterns in the search paths
223- let expanded_paths = expand_glob_patterns ( & search_paths) ;
212+ let ( config, search_scope) = build_refresh_config ( & refresh_options, config) ;
213+ if refresh_options. search_paths . is_some ( ) {
224214 trace ! (
225- "Expanded {} search paths to {} paths" ,
226- search_paths. len( ) ,
227- expanded_paths. len( )
228- ) ;
229- // These workspace folders are only for this refresh.
230- config. workspace_directories = Some (
231- expanded_paths
232- . iter ( )
233- . filter ( |p| p. is_dir ( ) )
234- . cloned ( )
235- . collect ( ) ,
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 )
236222 ) ;
237- config. executables = Some (
238- expanded_paths
239- . iter ( )
240- . filter ( |p| p. is_file ( ) )
241- . cloned ( )
242- . collect ( ) ,
243- ) ;
244- search_scope = Some ( SearchScope :: Workspace ) ;
223+ }
245224
246- // Configure the locators with the modified config.
247- for locator in context. locators . iter ( ) {
248- locator. configure ( & config) ;
249- }
250- } else if let Some ( search_kind) = refresh_options. search_kind {
251- // When only search_kind is provided, keep workspace directories so that
252- // workspace-based environments (Venv, VirtualEnv, etc.) can be found.
253- // The search_scope tells find_and_report_envs to filter locators by kind.
254- search_scope = Some ( SearchScope :: Global ( search_kind) ) ;
255-
256- // Re-configure the locators (config unchanged, but ensures consistency).
257- for locator in context. locators . iter ( ) {
258- locator. configure ( & config) ;
259- }
260- } else {
261- // Re-configure the locators with an un-modified config.
262- // Possible we congirued the locators with a modified config in the in the previous request.
263- // & the config was scoped to a particular search folder, executables or kind.
264- for locator in context. locators . iter ( ) {
265- locator. configure ( & config) ;
266- }
225+ // Configure the locators with the (possibly modified) config.
226+ for locator in context. locators . iter ( ) {
227+ locator. configure ( & config) ;
267228 }
268229
269230 trace ! ( "Start refreshing environments, config: {:?}" , config) ;
@@ -506,3 +467,152 @@ pub fn handle_clear_cache(_context: Arc<Context>, id: u32, _params: Value) {
506467 }
507468 } ) ;
508469}
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 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 original_workspace = PathBuf :: from ( "/original/workspace" ) ;
564+ let config = Configuration {
565+ workspace_directories : Some ( vec ! [ original_workspace] ) ,
566+ ..Default :: default ( )
567+ } ;
568+
569+ // Note: search_paths won't exist as directories in tests, so expanded result will be empty
570+ let refresh_options = RefreshOptions {
571+ search_kind : None ,
572+ search_paths : Some ( vec ! [ PathBuf :: from( "/search/path" ) ] ) ,
573+ } ;
574+
575+ let ( result_config, search_scope) = build_refresh_config ( & refresh_options, config) ;
576+
577+ // workspace_directories should be replaced (empty since paths don't exist)
578+ assert_eq ! (
579+ result_config. workspace_directories,
580+ Some ( vec![ ] ) ,
581+ "workspace_directories should be replaced by search_paths"
582+ ) ;
583+
584+ assert ! (
585+ matches!( search_scope, Some ( SearchScope :: Workspace ) ) ,
586+ "search_scope should be Workspace"
587+ ) ;
588+ }
589+
590+ /// Test that when neither searchKind nor searchPaths is provided,
591+ /// configuration is unchanged.
592+ #[ test]
593+ fn test_no_options_preserves_config ( ) {
594+ let workspace = PathBuf :: from ( "/test/workspace" ) ;
595+ let config = Configuration {
596+ workspace_directories : Some ( vec ! [ workspace. clone( ) ] ) ,
597+ ..Default :: default ( )
598+ } ;
599+
600+ let refresh_options = RefreshOptions {
601+ search_kind : None ,
602+ search_paths : None ,
603+ } ;
604+
605+ let ( result_config, search_scope) = build_refresh_config ( & refresh_options, config) ;
606+
607+ assert_eq ! (
608+ result_config. workspace_directories,
609+ Some ( vec![ workspace] ) ,
610+ "workspace_directories should be preserved when no options provided"
611+ ) ;
612+
613+ assert ! (
614+ search_scope. is_none( ) ,
615+ "search_scope should be None when no options provided"
616+ ) ;
617+ }
618+ }
0 commit comments