Fix toolchain detection for worktree-local paths (#20229)

Stanislav Alekseev created

Reimplements `pet::EnvironmentApi`, trying to access the `project_env`
first
Closes #20177 

Release Notes:

- Fixed python toolchain detection when worktree local path is set

Change summary

Cargo.lock                                   |  1 
Cargo.toml                                   |  1 
crates/language/src/toolchain.rs             |  7 +
crates/languages/Cargo.toml                  |  1 
crates/languages/src/python.rs               | 83 +++++++++++++++++++++
crates/project/src/project.rs                | 21 ++++-
crates/project/src/toolchain_store.rs        | 21 ++++
crates/remote_server/src/headless_project.rs | 10 ++
8 files changed, 132 insertions(+), 13 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -6403,6 +6403,7 @@ dependencies = [
  "pet",
  "pet-conda",
  "pet-core",
+ "pet-fs",
  "pet-poetry",
  "pet-reporter",
  "project",

Cargo.toml 🔗

@@ -381,6 +381,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
 parking_lot = "0.12.1"
 pathdiff = "0.2"
 pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
+pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
 pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c"  }
 pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c"  }
 pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c"  }

crates/language/src/toolchain.rs 🔗

@@ -7,6 +7,7 @@
 use std::{path::PathBuf, sync::Arc};
 
 use async_trait::async_trait;
+use collections::HashMap;
 use gpui::{AsyncAppContext, SharedString};
 use settings::WorktreeId;
 
@@ -23,7 +24,11 @@ pub struct Toolchain {
 
 #[async_trait(?Send)]
 pub trait ToolchainLister: Send + Sync {
-    async fn list(&self, _: PathBuf) -> ToolchainList;
+    async fn list(
+        &self,
+        worktree_root: PathBuf,
+        project_env: Option<HashMap<String, String>>,
+    ) -> ToolchainList;
 }
 
 #[async_trait(?Send)]

crates/languages/Cargo.toml 🔗

@@ -47,6 +47,7 @@ lsp.workspace = true
 node_runtime.workspace = true
 paths.workspace = true
 pet.workspace = true
+pet-fs.workspace = true
 pet-core.workspace = true
 pet-conda.workspace = true
 pet-poetry.workspace = true

crates/languages/src/python.rs 🔗

@@ -11,11 +11,13 @@ use language::ToolchainLister;
 use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
+use pet_core::os_environment::Environment;
 use pet_core::python_environment::PythonEnvironmentKind;
 use pet_core::Configuration;
 use project::lsp_store::language_server_settings;
 use serde_json::Value;
 
+use std::sync::Mutex;
 use std::{
     any::Any,
     borrow::Cow,
@@ -380,8 +382,13 @@ fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
 
 #[async_trait(?Send)]
 impl ToolchainLister for PythonToolchainProvider {
-    async fn list(&self, worktree_root: PathBuf) -> ToolchainList {
-        let environment = pet_core::os_environment::EnvironmentApi::new();
+    async fn list(
+        &self,
+        worktree_root: PathBuf,
+        project_env: Option<HashMap<String, String>>,
+    ) -> ToolchainList {
+        let env = project_env.unwrap_or_default();
+        let environment = EnvironmentApi::from_env(&env);
         let locators = pet::locators::create_locators(
             Arc::new(pet_conda::Conda::from(&environment)),
             Arc::new(pet_poetry::Poetry::from(&environment)),
@@ -427,6 +434,78 @@ impl ToolchainLister for PythonToolchainProvider {
     }
 }
 
+pub struct EnvironmentApi<'a> {
+    global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
+    project_env: &'a HashMap<String, String>,
+    pet_env: pet_core::os_environment::EnvironmentApi,
+}
+
+impl<'a> EnvironmentApi<'a> {
+    pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
+        let paths = project_env
+            .get("PATH")
+            .map(|p| std::env::split_paths(p).collect())
+            .unwrap_or_default();
+
+        EnvironmentApi {
+            global_search_locations: Arc::new(Mutex::new(paths)),
+            project_env,
+            pet_env: pet_core::os_environment::EnvironmentApi::new(),
+        }
+    }
+
+    fn user_home(&self) -> Option<PathBuf> {
+        self.project_env
+            .get("HOME")
+            .or_else(|| self.project_env.get("USERPROFILE"))
+            .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
+            .or_else(|| self.pet_env.get_user_home())
+    }
+}
+
+impl<'a> pet_core::os_environment::Environment for EnvironmentApi<'a> {
+    fn get_user_home(&self) -> Option<PathBuf> {
+        self.user_home()
+    }
+
+    fn get_root(&self) -> Option<PathBuf> {
+        None
+    }
+
+    fn get_env_var(&self, key: String) -> Option<String> {
+        self.project_env
+            .get(&key)
+            .cloned()
+            .or_else(|| self.pet_env.get_env_var(key))
+    }
+
+    fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
+        if self.global_search_locations.lock().unwrap().is_empty() {
+            let mut paths =
+                std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
+                    .collect::<Vec<PathBuf>>();
+
+            log::trace!("Env PATH: {:?}", paths);
+            for p in self.pet_env.get_know_global_search_locations() {
+                if !paths.contains(&p) {
+                    paths.push(p);
+                }
+            }
+
+            let mut paths = paths
+                .into_iter()
+                .filter(|p| p.exists())
+                .collect::<Vec<PathBuf>>();
+
+            self.global_search_locations
+                .lock()
+                .unwrap()
+                .append(&mut paths);
+        }
+        self.global_search_locations.lock().unwrap().clone()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};

crates/project/src/project.rs 🔗

@@ -639,7 +639,12 @@ impl Project {
             cx.subscribe(&settings_observer, Self::on_settings_observer_event)
                 .detach();
             let toolchain_store = cx.new_model(|cx| {
-                ToolchainStore::local(languages.clone(), worktree_store.clone(), cx)
+                ToolchainStore::local(
+                    languages.clone(),
+                    worktree_store.clone(),
+                    environment.clone(),
+                    cx,
+                )
             });
             let lsp_store = cx.new_model(|cx| {
                 LspStore::new_local(
@@ -2369,10 +2374,16 @@ impl Project {
         language_name: LanguageName,
         cx: &AppContext,
     ) -> Task<Option<ToolchainList>> {
-        if let Some(toolchain_store) = self.toolchain_store.as_ref() {
-            toolchain_store
-                .read(cx)
-                .list_toolchains(worktree_id, language_name, cx)
+        if let Some(toolchain_store) = self.toolchain_store.clone() {
+            cx.spawn(|cx| async move {
+                cx.update(|cx| {
+                    toolchain_store
+                        .read(cx)
+                        .list_toolchains(worktree_id, language_name, cx)
+                })
+                .unwrap_or(Task::Ready(None))
+                .await
+            })
         } else {
             Task::ready(None)
         }

crates/project/src/toolchain_store.rs 🔗

@@ -13,7 +13,7 @@ use rpc::{proto, AnyProtoClient, TypedEnvelope};
 use settings::WorktreeId;
 use util::ResultExt as _;
 
-use crate::worktree_store::WorktreeStore;
+use crate::{worktree_store::WorktreeStore, ProjectEnvironment};
 
 pub struct ToolchainStore(ToolchainStoreInner);
 enum ToolchainStoreInner {
@@ -32,11 +32,13 @@ impl ToolchainStore {
     pub fn local(
         languages: Arc<LanguageRegistry>,
         worktree_store: Model<WorktreeStore>,
+        project_environment: Model<ProjectEnvironment>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let model = cx.new_model(|_| LocalToolchainStore {
             languages,
             worktree_store,
+            project_environment,
             active_toolchains: Default::default(),
         });
         let subscription = cx.subscribe(&model, |_, _, e: &ToolchainStoreEvent, cx| {
@@ -203,6 +205,7 @@ impl ToolchainStore {
 struct LocalToolchainStore {
     languages: Arc<LanguageRegistry>,
     worktree_store: Model<WorktreeStore>,
+    project_environment: Model<ProjectEnvironment>,
     active_toolchains: BTreeMap<(WorktreeId, LanguageName), Toolchain>,
 }
 
@@ -296,9 +299,20 @@ impl LocalToolchainStore {
         else {
             return Task::ready(None);
         };
-        cx.spawn(|_| async move {
+
+        let environment = self.project_environment.clone();
+        cx.spawn(|mut cx| async move {
+            let project_env = environment
+                .update(&mut cx, |environment, cx| {
+                    environment.get_environment(Some(worktree_id), Some(root.clone()), cx)
+                })
+                .ok()?
+                .await;
             let language = registry.language_for_name(&language_name.0).await.ok()?;
-            let toolchains = language.toolchain_lister()?.list(root.to_path_buf()).await;
+            let toolchains = language
+                .toolchain_lister()?
+                .list(root.to_path_buf(), project_env)
+                .await;
             Some(toolchains)
         })
     }
@@ -345,6 +359,7 @@ impl RemoteToolchainStore {
             Some(())
         })
     }
+
     pub(crate) fn list_toolchains(
         &self,
         worktree_id: WorktreeId,

crates/remote_server/src/headless_project.rs 🔗

@@ -108,8 +108,14 @@ impl HeadlessProject {
             observer.shared(SSH_PROJECT_ID, session.clone().into(), cx);
             observer
         });
-        let toolchain_store =
-            cx.new_model(|cx| ToolchainStore::local(languages.clone(), worktree_store.clone(), cx));
+        let toolchain_store = cx.new_model(|cx| {
+            ToolchainStore::local(
+                languages.clone(),
+                worktree_store.clone(),
+                environment.clone(),
+                cx,
+            )
+        });
         let lsp_store = cx.new_model(|cx| {
             let mut lsp_store = LspStore::new_local(
                 buffer_store.clone(),