diff --git a/crates/pet/src/find.rs b/crates/pet/src/find.rs index 30d9a631..59a1eb9d 100644 --- a/crates/pet/src/find.rs +++ b/crates/pet/src/find.rs @@ -653,4 +653,88 @@ mod tests { "canonicalize() would resolve to target, but path() does not" ); } + + /// Test for https://github.com/microsoft/python-environment-tools/issues/151 + /// Verifies that refresh with searchKind (e.g., "Venv") still finds environments + /// in workspace directories, not just global locations. + /// + /// The bug was that when searchKind was provided, workspace_directories was cleared, + /// preventing discovery of workspace-based environments like venvs. + #[test] + fn test_search_kind_finds_workspace_venvs() { + use super::{find_and_report_envs, SearchScope}; + use crate::locators::create_locators; + use pet_conda::Conda; + use pet_core::os_environment::EnvironmentApi; + use pet_core::python_environment::PythonEnvironmentKind; + use pet_core::Configuration; + use pet_poetry::Poetry; + use pet_reporter::collect; + use std::sync::Arc; + + let tmp = TempDir::new().expect("Failed to create temp dir"); + let workspace = tmp.path().to_path_buf(); + + // Create a venv structure in the workspace + let venv_dir = workspace.join(".venv"); + #[cfg(windows)] + let bin_dir = venv_dir.join("Scripts"); + #[cfg(unix)] + let bin_dir = venv_dir.join("bin"); + fs::create_dir_all(&bin_dir).expect("Failed to create bin dir"); + + // Create pyvenv.cfg (required for venv detection) + fs::write( + venv_dir.join("pyvenv.cfg"), + "home = /usr/bin\nversion = 3.11.0\n", + ) + .expect("Failed to create pyvenv.cfg"); + + // Create python executable + #[cfg(windows)] + let python_exe = bin_dir.join("python.exe"); + #[cfg(unix)] + let python_exe = bin_dir.join("python"); + fs::write(&python_exe, "fake python").expect("Failed to create python exe"); + + // Set up locators and configuration + let environment = EnvironmentApi::new(); + let conda_locator = Arc::new(Conda::from(&environment)); + let poetry_locator = Arc::new(Poetry::from(&environment)); + let locators = create_locators(conda_locator, poetry_locator, &environment); + + let config = Configuration { + workspace_directories: Some(vec![workspace.clone()]), + ..Default::default() + }; + for locator in locators.iter() { + locator.configure(&config); + } + + let reporter = Arc::new(collect::create_reporter()); + + // Call find_and_report_envs with SearchScope::Global(Venv) + // This simulates the behavior when refresh is called with searchKind: "Venv" + find_and_report_envs( + reporter.as_ref(), + config, + &locators, + &environment, + Some(SearchScope::Global(PythonEnvironmentKind::Venv)), + ); + + let environments = reporter.environments.lock().unwrap(); + + // The venv should be discovered even when searching by kind + let venv_found = environments.iter().any(|env| { + env.kind == Some(PythonEnvironmentKind::Venv) + && env.prefix.as_ref().map(|p| p == &venv_dir).unwrap_or(false) + }); + + assert!( + venv_found, + "Venv in workspace should be found when searching by kind. Found environments: {:?}", + *environments + ); + } } diff --git a/crates/pet/src/jsonrpc.rs b/crates/pet/src/jsonrpc.rs index 3b056a21..fa7e04c3 100644 --- a/crates/pet/src/jsonrpc.rs +++ b/crates/pet/src/jsonrpc.rs @@ -211,43 +211,52 @@ pub fn handle_refresh(context: Arc, id: u32, params: Value) { let mut search_scope = None; - // If search kind is provided and no search_paths, then we will only search in the global locations. - if refresh_options.search_kind.is_some() || refresh_options.search_paths.is_some() { - // Always clear this, as we will either serach in specified folder or a specific kind in global locations. + // If search_paths is provided, limit search to those paths. + // If only search_kind is provided (without search_paths), we still search + // workspace directories because many environment types (like Venv, VirtualEnv) + // don't have global locations - they only exist in workspace folders. + // The reporter will filter results to only report the requested kind. + if let Some(search_paths) = refresh_options.search_paths { + // Clear workspace directories when explicit search paths are provided. config.workspace_directories = None; - if let Some(search_paths) = refresh_options.search_paths { - // Expand any glob patterns in the search paths - let expanded_paths = expand_glob_patterns(&search_paths); - trace!( - "Expanded {} search paths to {} paths", - search_paths.len(), - expanded_paths.len() - ); - // These workspace folders are only for this refresh. - config.workspace_directories = Some( - expanded_paths - .iter() - .filter(|p| p.is_dir()) - .cloned() - .collect(), - ); - config.executables = Some( - expanded_paths - .iter() - .filter(|p| p.is_file()) - .cloned() - .collect(), - ); - search_scope = Some(SearchScope::Workspace); - } else if let Some(search_kind) = refresh_options.search_kind { - config.executables = None; - search_scope = Some(SearchScope::Global(search_kind)); - } + // Expand any glob patterns in the search paths + let expanded_paths = expand_glob_patterns(&search_paths); + trace!( + "Expanded {} search paths to {} paths", + search_paths.len(), + expanded_paths.len() + ); + // These workspace folders are only for this refresh. + config.workspace_directories = Some( + expanded_paths + .iter() + .filter(|p| p.is_dir()) + .cloned() + .collect(), + ); + config.executables = Some( + expanded_paths + .iter() + .filter(|p| p.is_file()) + .cloned() + .collect(), + ); + search_scope = Some(SearchScope::Workspace); // Configure the locators with the modified config. for locator in context.locators.iter() { locator.configure(&config); } + } else if let Some(search_kind) = refresh_options.search_kind { + // When only search_kind is provided, keep workspace directories so that + // workspace-based environments (Venv, VirtualEnv, etc.) can be found. + // The search_scope tells find_and_report_envs to filter locators by kind. + search_scope = Some(SearchScope::Global(search_kind)); + + // Re-configure the locators (config unchanged, but ensures consistency). + for locator in context.locators.iter() { + locator.configure(&config); + } } else { // Re-configure the locators with an un-modified config. // Possible we congirued the locators with a modified config in the in the previous request.