python: Re-land usage of source file path in toolchain picker (#31893)

Piotr Osiewicz created

This reverts commit 1e55e88c1822402566bdec8a893ec0429d8ee5e6.

Closes #ISSUE

Release Notes:

- Python toolchain selector now uses path to the closest pyproject.toml
as a basis for picking a toolchain. All files under the same
pyproject.toml (in filesystem hierarchy) will share a single virtual
environment. It is possible to have multiple Python virtual environments
selected for disjoint parts of the same project.

Change summary

crates/language/src/language.rs                     |   3 
crates/language/src/manifest.rs                     |  10 
crates/language/src/toolchain.rs                    |   5 
crates/languages/src/python.rs                      |  13 +
crates/project/src/lsp_store.rs                     |  30 +-
crates/project/src/manifest_tree.rs                 |  34 +++
crates/project/src/manifest_tree/server_tree.rs     |   8 
crates/project/src/project.rs                       |  19 +-
crates/project/src/toolchain_store.rs               | 123 ++++++++++----
crates/proto/proto/toolchain.proto                  |   1 
crates/remote_server/src/headless_project.rs        |   8 
crates/repl/src/kernels/mod.rs                      |   2 
crates/toolchain_selector/src/active_toolchain.rs   |   2 
crates/toolchain_selector/src/toolchain_selector.rs |  29 ++-
14 files changed, 195 insertions(+), 92 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -34,7 +34,7 @@ pub use highlight_map::HighlightMap;
 use http_client::HttpClient;
 pub use language_registry::{LanguageName, LoadedLanguage};
 use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};
-pub use manifest::{ManifestName, ManifestProvider, ManifestQuery};
+pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
 use parking_lot::Mutex;
 use regex::Regex;
 use schemars::{
@@ -323,7 +323,6 @@ pub trait LspAdapterDelegate: Send + Sync {
     fn http_client(&self) -> Arc<dyn HttpClient>;
     fn worktree_id(&self) -> WorktreeId;
     fn worktree_root_path(&self) -> &Path;
-    fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
     fn update_status(&self, language: LanguageServerName, status: BinaryStatus);
     fn registered_lsp_adapters(&self) -> Vec<Arc<dyn LspAdapter>>;
     async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;

crates/language/src/manifest.rs 🔗

@@ -1,8 +1,7 @@
 use std::{borrow::Borrow, path::Path, sync::Arc};
 
 use gpui::SharedString;
-
-use crate::LspAdapterDelegate;
+use settings::WorktreeId;
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct ManifestName(SharedString);
@@ -39,10 +38,15 @@ pub struct ManifestQuery {
     /// Path to the file, relative to worktree root.
     pub path: Arc<Path>,
     pub depth: usize,
-    pub delegate: Arc<dyn LspAdapterDelegate>,
+    pub delegate: Arc<dyn ManifestDelegate>,
 }
 
 pub trait ManifestProvider {
     fn name(&self) -> ManifestName;
     fn search(&self, query: ManifestQuery) -> Option<Arc<Path>>;
 }
+
+pub trait ManifestDelegate: Send + Sync {
+    fn worktree_id(&self) -> WorktreeId;
+    fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
+}

crates/language/src/toolchain.rs 🔗

@@ -14,7 +14,7 @@ use collections::HashMap;
 use gpui::{AsyncApp, SharedString};
 use settings::WorktreeId;
 
-use crate::LanguageName;
+use crate::{LanguageName, ManifestName};
 
 /// Represents a single toolchain.
 #[derive(Clone, Debug)]
@@ -44,10 +44,13 @@ pub trait ToolchainLister: Send + Sync {
     async fn list(
         &self,
         worktree_root: PathBuf,
+        subroot_relative_path: Option<Arc<Path>>,
         project_env: Option<HashMap<String, String>>,
     ) -> ToolchainList;
     // Returns a term which we should use in UI to refer to a toolchain.
     fn term(&self) -> SharedString;
+    /// Returns the name of the manifest file for this toolchain.
+    fn manifest_name(&self) -> ManifestName;
 }
 
 #[async_trait(?Send)]

crates/languages/src/python.rs 🔗

@@ -693,9 +693,13 @@ fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
 
 #[async_trait]
 impl ToolchainLister for PythonToolchainProvider {
+    fn manifest_name(&self) -> language::ManifestName {
+        ManifestName::from(SharedString::new_static("pyproject.toml"))
+    }
     async fn list(
         &self,
         worktree_root: PathBuf,
+        subroot_relative_path: Option<Arc<Path>>,
         project_env: Option<HashMap<String, String>>,
     ) -> ToolchainList {
         let env = project_env.unwrap_or_default();
@@ -706,7 +710,14 @@ impl ToolchainLister for PythonToolchainProvider {
             &environment,
         );
         let mut config = Configuration::default();
-        config.workspace_directories = Some(vec![worktree_root.clone()]);
+
+        let mut directories = vec![worktree_root.clone()];
+        if let Some(subroot_relative_path) = subroot_relative_path {
+            debug_assert!(subroot_relative_path.is_relative());
+            directories.push(worktree_root.join(subroot_relative_path));
+        }
+
+        config.workspace_directories = Some(directories);
         for locator in locators.iter() {
             locator.configure(&config);
         }

crates/project/src/lsp_store.rs 🔗

@@ -10,7 +10,8 @@ use crate::{
     lsp_command::{self, *},
     lsp_store,
     manifest_tree::{
-        AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestTree,
+        AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition,
+        ManifestQueryDelegate, ManifestTree,
     },
     prettier_store::{self, PrettierStore, PrettierStoreEvent},
     project_settings::{LspSettings, ProjectSettings},
@@ -1036,7 +1037,7 @@ impl LocalLspStore {
         else {
             return Vec::new();
         };
-        let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
+        let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot()));
         let root = self.lsp_tree.update(cx, |this, cx| {
             this.get(
                 project_path,
@@ -2290,7 +2291,8 @@ impl LocalLspStore {
             })
             .map(|(delegate, servers)| (true, delegate, servers))
             .unwrap_or_else(|| {
-                let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
+                let lsp_delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
+                let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot()));
                 let servers = self
                     .lsp_tree
                     .clone()
@@ -2304,7 +2306,7 @@ impl LocalLspStore {
                             )
                             .collect::<Vec<_>>()
                     });
-                (false, delegate, servers)
+                (false, lsp_delegate, servers)
             });
         let servers = servers
             .into_iter()
@@ -3585,6 +3587,7 @@ impl LspStore {
         prettier_store: Entity<PrettierStore>,
         toolchain_store: Entity<ToolchainStore>,
         environment: Entity<ProjectEnvironment>,
+        manifest_tree: Entity<ManifestTree>,
         languages: Arc<LanguageRegistry>,
         http_client: Arc<dyn HttpClient>,
         fs: Arc<dyn Fs>,
@@ -3618,7 +3621,7 @@ impl LspStore {
                 sender,
             )
         };
-        let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
+
         Self {
             mode: LspStoreMode::Local(LocalLspStore {
                 weak: cx.weak_entity(),
@@ -4465,10 +4468,13 @@ impl LspStore {
                             )
                             .map(|(delegate, servers)| (true, delegate, servers))
                             .or_else(|| {
-                                let delegate = adapters
+                                let lsp_delegate = adapters
                                     .entry(worktree_id)
                                     .or_insert_with(|| get_adapter(worktree_id, cx))
                                     .clone()?;
+                                let delegate = Arc::new(ManifestQueryDelegate::new(
+                                    worktree.read(cx).snapshot(),
+                                ));
                                 let path = file
                                     .path()
                                     .parent()
@@ -4483,7 +4489,7 @@ impl LspStore {
                                     cx,
                                 );
 
-                                Some((false, delegate, nodes.collect()))
+                                Some((false, lsp_delegate, nodes.collect()))
                             })
                         else {
                             continue;
@@ -6476,7 +6482,7 @@ impl LspStore {
                 worktree_id,
                 path: Arc::from("".as_ref()),
             };
-            let delegate = LocalLspAdapterDelegate::from_local_lsp(local, &worktree, cx);
+            let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot()));
             local.lsp_tree.update(cx, |language_server_tree, cx| {
                 for node in language_server_tree.get(
                     path,
@@ -10204,14 +10210,6 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
         self.worktree.id()
     }
 
-    fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool {
-        self.worktree.entry_for_path(path).map_or(false, |entry| {
-            is_dir.map_or(true, |is_required_to_be_dir| {
-                is_required_to_be_dir == entry.is_dir()
-            })
-        })
-    }
-
     fn worktree_root_path(&self) -> &Path {
         self.worktree.abs_path().as_ref()
     }

crates/project/src/manifest_tree.rs 🔗

@@ -11,16 +11,17 @@ use std::{
     borrow::Borrow,
     collections::{BTreeMap, hash_map::Entry},
     ops::ControlFlow,
+    path::Path,
     sync::Arc,
 };
 
 use collections::HashMap;
 use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription};
-use language::{LspAdapterDelegate, ManifestName, ManifestQuery};
+use language::{ManifestDelegate, ManifestName, ManifestQuery};
 pub use manifest_store::ManifestProviders;
 use path_trie::{LabelPresence, RootPathTrie, TriePath};
 use settings::{SettingsStore, WorktreeId};
-use worktree::{Event as WorktreeEvent, Worktree};
+use worktree::{Event as WorktreeEvent, Snapshot, Worktree};
 
 use crate::{
     ProjectPath,
@@ -89,7 +90,7 @@ pub(crate) enum ManifestTreeEvent {
 impl EventEmitter<ManifestTreeEvent> for ManifestTree {}
 
 impl ManifestTree {
-    pub(crate) fn new(worktree_store: Entity<WorktreeStore>, cx: &mut App) -> Entity<Self> {
+    pub fn new(worktree_store: Entity<WorktreeStore>, cx: &mut App) -> Entity<Self> {
         cx.new(|cx| Self {
             root_points: Default::default(),
             _subscriptions: [
@@ -106,11 +107,11 @@ impl ManifestTree {
             worktree_store,
         })
     }
-    fn root_for_path(
+    pub(crate) fn root_for_path(
         &mut self,
         ProjectPath { worktree_id, path }: ProjectPath,
         manifests: &mut dyn Iterator<Item = ManifestName>,
-        delegate: Arc<dyn LspAdapterDelegate>,
+        delegate: Arc<dyn ManifestDelegate>,
         cx: &mut App,
     ) -> BTreeMap<ManifestName, ProjectPath> {
         debug_assert_eq!(delegate.worktree_id(), worktree_id);
@@ -218,3 +219,26 @@ impl ManifestTree {
         }
     }
 }
+
+pub(crate) struct ManifestQueryDelegate {
+    worktree: Snapshot,
+}
+impl ManifestQueryDelegate {
+    pub fn new(worktree: Snapshot) -> Self {
+        Self { worktree }
+    }
+}
+
+impl ManifestDelegate for ManifestQueryDelegate {
+    fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool {
+        self.worktree.entry_for_path(path).map_or(false, |entry| {
+            is_dir.map_or(true, |is_required_to_be_dir| {
+                is_required_to_be_dir == entry.is_dir()
+            })
+        })
+    }
+
+    fn worktree_id(&self) -> WorktreeId {
+        self.worktree.id()
+    }
+}

crates/project/src/manifest_tree/server_tree.rs 🔗

@@ -16,7 +16,7 @@ use std::{
 use collections::{HashMap, IndexMap};
 use gpui::{App, AppContext as _, Entity, Subscription};
 use language::{
-    Attach, CachedLspAdapter, LanguageName, LanguageRegistry, LspAdapterDelegate,
+    Attach, CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate,
     language_settings::AllLanguageSettings,
 };
 use lsp::LanguageServerName;
@@ -151,7 +151,7 @@ impl LanguageServerTree {
         &'a mut self,
         path: ProjectPath,
         query: AdapterQuery<'_>,
-        delegate: Arc<dyn LspAdapterDelegate>,
+        delegate: Arc<dyn ManifestDelegate>,
         cx: &mut App,
     ) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
         let settings_location = SettingsLocation {
@@ -181,7 +181,7 @@ impl LanguageServerTree {
             LanguageServerName,
             (LspSettings, BTreeSet<LanguageName>, Arc<CachedLspAdapter>),
         >,
-        delegate: Arc<dyn LspAdapterDelegate>,
+        delegate: Arc<dyn ManifestDelegate>,
         cx: &mut App,
     ) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
         let worktree_id = path.worktree_id;
@@ -401,7 +401,7 @@ impl<'tree> ServerTreeRebase<'tree> {
         &'a mut self,
         path: ProjectPath,
         query: AdapterQuery<'_>,
-        delegate: Arc<dyn LspAdapterDelegate>,
+        delegate: Arc<dyn ManifestDelegate>,
         cx: &mut App,
     ) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
         let settings_location = SettingsLocation {

crates/project/src/project.rs 🔗

@@ -35,6 +35,7 @@ pub use git_store::{
     ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
     git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
 };
+pub use manifest_tree::ManifestTree;
 
 use anyhow::{Context as _, Result, anyhow};
 use buffer_store::{BufferStore, BufferStoreEvent};
@@ -874,11 +875,13 @@ impl Project {
                 cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx));
 
             let environment = cx.new(|_| ProjectEnvironment::new(env));
+            let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
             let toolchain_store = cx.new(|cx| {
                 ToolchainStore::local(
                     languages.clone(),
                     worktree_store.clone(),
                     environment.clone(),
+                    manifest_tree.clone(),
                     cx,
                 )
             });
@@ -946,6 +949,7 @@ impl Project {
                     prettier_store.clone(),
                     toolchain_store.clone(),
                     environment.clone(),
+                    manifest_tree,
                     languages.clone(),
                     client.http_client(),
                     fs.clone(),
@@ -3084,16 +3088,13 @@ impl Project {
         path: ProjectPath,
         language_name: LanguageName,
         cx: &App,
-    ) -> Task<Option<ToolchainList>> {
-        if let Some(toolchain_store) = self.toolchain_store.clone() {
+    ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
+        if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) {
             cx.spawn(async move |cx| {
-                cx.update(|cx| {
-                    toolchain_store
-                        .read(cx)
-                        .list_toolchains(path, language_name, cx)
-                })
-                .ok()?
-                .await
+                toolchain_store
+                    .update(cx, |this, cx| this.list_toolchains(path, language_name, cx))
+                    .ok()?
+                    .await
             })
         } else {
             Task::ready(None)

crates/project/src/toolchain_store.rs 🔗

@@ -19,7 +19,11 @@ use rpc::{
 use settings::WorktreeId;
 use util::ResultExt as _;
 
-use crate::{ProjectEnvironment, ProjectPath, worktree_store::WorktreeStore};
+use crate::{
+    ProjectEnvironment, ProjectPath,
+    manifest_tree::{ManifestQueryDelegate, ManifestTree},
+    worktree_store::WorktreeStore,
+};
 
 pub struct ToolchainStore(ToolchainStoreInner);
 enum ToolchainStoreInner {
@@ -42,6 +46,7 @@ impl ToolchainStore {
         languages: Arc<LanguageRegistry>,
         worktree_store: Entity<WorktreeStore>,
         project_environment: Entity<ProjectEnvironment>,
+        manifest_tree: Entity<ManifestTree>,
         cx: &mut Context<Self>,
     ) -> Self {
         let entity = cx.new(|_| LocalToolchainStore {
@@ -49,6 +54,7 @@ impl ToolchainStore {
             worktree_store,
             project_environment,
             active_toolchains: Default::default(),
+            manifest_tree,
         });
         let subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
             cx.emit(e.clone())
@@ -80,11 +86,11 @@ impl ToolchainStore {
         &self,
         path: ProjectPath,
         language_name: LanguageName,
-        cx: &App,
-    ) -> Task<Option<ToolchainList>> {
+        cx: &mut Context<Self>,
+    ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
         match &self.0 {
             ToolchainStoreInner::Local(local, _) => {
-                local.read(cx).list_toolchains(path, language_name, cx)
+                local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx))
             }
             ToolchainStoreInner::Remote(remote) => {
                 remote.read(cx).list_toolchains(path, language_name, cx)
@@ -181,7 +187,7 @@ impl ToolchainStore {
             })?
             .await;
         let has_values = toolchains.is_some();
-        let groups = if let Some(toolchains) = &toolchains {
+        let groups = if let Some((toolchains, _)) = &toolchains {
             toolchains
                 .groups
                 .iter()
@@ -195,8 +201,8 @@ impl ToolchainStore {
         } else {
             vec![]
         };
-        let toolchains = if let Some(toolchains) = toolchains {
-            toolchains
+        let (toolchains, relative_path) = if let Some((toolchains, relative_path)) = toolchains {
+            let toolchains = toolchains
                 .toolchains
                 .into_iter()
                 .map(|toolchain| {
@@ -207,15 +213,17 @@ impl ToolchainStore {
                         raw_json: toolchain.as_json.to_string(),
                     }
                 })
-                .collect::<Vec<_>>()
+                .collect::<Vec<_>>();
+            (toolchains, relative_path)
         } else {
-            vec![]
+            (vec![], Arc::from(Path::new("")))
         };
 
         Ok(proto::ListToolchainsResponse {
             has_values,
             toolchains,
             groups,
+            relative_worktree_path: Some(relative_path.to_string_lossy().into_owned()),
         })
     }
     pub fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
@@ -231,6 +239,7 @@ struct LocalToolchainStore {
     worktree_store: Entity<WorktreeStore>,
     project_environment: Entity<ProjectEnvironment>,
     active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap<Arc<Path>, Toolchain>>,
+    manifest_tree: Entity<ManifestTree>,
 }
 
 #[async_trait(?Send)]
@@ -312,36 +321,73 @@ impl LocalToolchainStore {
         })
     }
     pub(crate) fn list_toolchains(
-        &self,
+        &mut self,
         path: ProjectPath,
         language_name: LanguageName,
-        cx: &App,
-    ) -> Task<Option<ToolchainList>> {
+        cx: &mut Context<Self>,
+    ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
         let registry = self.languages.clone();
-        let Some(abs_path) = self
-            .worktree_store
-            .read(cx)
-            .worktree_for_id(path.worktree_id, cx)
-            .map(|worktree| worktree.read(cx).abs_path())
-        else {
-            return Task::ready(None);
-        };
+
+        let manifest_tree = self.manifest_tree.downgrade();
+
         let environment = self.project_environment.clone();
-        cx.spawn(async move |cx| {
+        cx.spawn(async move |this, cx| {
+            let language = cx
+                .background_spawn(registry.language_for_name(language_name.as_ref()))
+                .await
+                .ok()?;
+            let toolchains = language.toolchain_lister()?;
+            let manifest_name = toolchains.manifest_name();
+            let (snapshot, worktree) = this
+                .update(cx, |this, cx| {
+                    this.worktree_store
+                        .read(cx)
+                        .worktree_for_id(path.worktree_id, cx)
+                        .map(|worktree| (worktree.read(cx).snapshot(), worktree))
+                })
+                .ok()
+                .flatten()?;
+            let worktree_id = snapshot.id();
+            let worktree_root = snapshot.abs_path().to_path_buf();
+            let relative_path = manifest_tree
+                .update(cx, |this, cx| {
+                    this.root_for_path(
+                        path,
+                        &mut std::iter::once(manifest_name.clone()),
+                        Arc::new(ManifestQueryDelegate::new(snapshot)),
+                        cx,
+                    )
+                })
+                .ok()?
+                .remove(&manifest_name)
+                .unwrap_or_else(|| ProjectPath {
+                    path: Arc::from(Path::new("")),
+                    worktree_id,
+                });
+            let abs_path = worktree
+                .update(cx, |this, _| this.absolutize(&relative_path.path).ok())
+                .ok()
+                .flatten()?;
+
             let project_env = environment
                 .update(cx, |environment, cx| {
-                    environment.get_directory_environment(abs_path.clone(), cx)
+                    environment.get_directory_environment(abs_path.as_path().into(), cx)
                 })
                 .ok()?
                 .await;
 
             cx.background_spawn(async move {
-                let language = registry
-                    .language_for_name(language_name.as_ref())
-                    .await
-                    .ok()?;
-                let toolchains = language.toolchain_lister()?;
-                Some(toolchains.list(abs_path.to_path_buf(), project_env).await)
+                Some((
+                    toolchains
+                        .list(
+                            worktree_root,
+                            Some(relative_path.path.clone())
+                                .filter(|_| *relative_path.path != *Path::new("")),
+                            project_env,
+                        )
+                        .await,
+                    relative_path.path,
+                ))
             })
             .await
         })
@@ -404,7 +450,7 @@ impl RemoteToolchainStore {
         path: ProjectPath,
         language_name: LanguageName,
         cx: &App,
-    ) -> Task<Option<ToolchainList>> {
+    ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
         let project_id = self.project_id;
         let client = self.client.clone();
         cx.background_spawn(async move {
@@ -444,11 +490,20 @@ impl RemoteToolchainStore {
                     Some((usize::try_from(group.start_index).ok()?, group.name.into()))
                 })
                 .collect();
-            Some(ToolchainList {
-                toolchains,
-                default: None,
-                groups,
-            })
+            let relative_path = Arc::from(Path::new(
+                response
+                    .relative_worktree_path
+                    .as_deref()
+                    .unwrap_or_default(),
+            ));
+            Some((
+                ToolchainList {
+                    toolchains,
+                    default: None,
+                    groups,
+                },
+                relative_path,
+            ))
         })
     }
     pub(crate) fn active_toolchain(

crates/proto/proto/toolchain.proto 🔗

@@ -23,6 +23,7 @@ message ListToolchainsResponse {
     repeated Toolchain toolchains = 1;
     bool has_values = 2;
     repeated ToolchainGroup groups = 3;
+    optional string relative_worktree_path = 4;
 }
 
 message ActivateToolchain {

crates/remote_server/src/headless_project.rs 🔗

@@ -9,8 +9,8 @@ use http_client::HttpClient;
 use language::{Buffer, BufferEvent, LanguageRegistry, proto::serialize_operation};
 use node_runtime::NodeRuntime;
 use project::{
-    LspStore, LspStoreEvent, PrettierStore, ProjectEnvironment, ProjectPath, ToolchainStore,
-    WorktreeId,
+    LspStore, LspStoreEvent, ManifestTree, PrettierStore, ProjectEnvironment, ProjectPath,
+    ToolchainStore, WorktreeId,
     buffer_store::{BufferStore, BufferStoreEvent},
     debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore},
     git_store::GitStore,
@@ -87,12 +87,13 @@ impl HeadlessProject {
         });
 
         let environment = cx.new(|_| ProjectEnvironment::new(None));
-
+        let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
         let toolchain_store = cx.new(|cx| {
             ToolchainStore::local(
                 languages.clone(),
                 worktree_store.clone(),
                 environment.clone(),
+                manifest_tree.clone(),
                 cx,
             )
         });
@@ -172,6 +173,7 @@ impl HeadlessProject {
                 prettier_store.clone(),
                 toolchain_store.clone(),
                 environment,
+                manifest_tree,
                 languages.clone(),
                 http_client.clone(),
                 fs.clone(),

crates/repl/src/kernels/mod.rs 🔗

@@ -92,7 +92,7 @@ pub fn python_env_kernel_specifications(
     let background_executor = cx.background_executor().clone();
 
     async move {
-        let toolchains = if let Some(toolchains) = toolchains.await {
+        let toolchains = if let Some((toolchains, _)) = toolchains.await {
             toolchains
         } else {
             return Ok(Vec::new());

crates/toolchain_selector/src/active_toolchain.rs 🔗

@@ -158,7 +158,7 @@ impl ActiveToolchain {
                 let project = workspace
                     .read_with(cx, |this, _| this.project().clone())
                     .ok()?;
-                let toolchains = cx
+                let (toolchains, relative_path) = cx
                     .update(|_, cx| {
                         project.read(cx).available_toolchains(
                             ProjectPath {

crates/toolchain_selector/src/toolchain_selector.rs 🔗

@@ -10,7 +10,7 @@ use gpui::{
 use language::{LanguageName, Toolchain, ToolchainList};
 use picker::{Picker, PickerDelegate};
 use project::{Project, ProjectPath, WorktreeId};
-use std::{path::Path, sync::Arc};
+use std::{borrow::Cow, path::Path, sync::Arc};
 use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
 use util::ResultExt;
 use workspace::{ModalView, Workspace};
@@ -172,18 +172,8 @@ impl ToolchainSelectorDelegate {
                 let relative_path = this
                     .read_with(cx, |this, _| this.delegate.relative_path.clone())
                     .ok()?;
-                let placeholder_text = format!(
-                    "Select a {} for `{}`…",
-                    term.to_lowercase(),
-                    relative_path.to_string_lossy()
-                )
-                .into();
-                let _ = this.update_in(cx, move |this, window, cx| {
-                    this.delegate.placeholder_text = placeholder_text;
-                    this.refresh_placeholder(window, cx);
-                });
 
-                let available_toolchains = project
+                let (available_toolchains, relative_path) = project
                     .update(cx, |this, cx| {
                         this.available_toolchains(
                             ProjectPath {
@@ -196,6 +186,21 @@ impl ToolchainSelectorDelegate {
                     })
                     .ok()?
                     .await?;
+                let pretty_path = {
+                    let path = relative_path.to_string_lossy();
+                    if path.is_empty() {
+                        Cow::Borrowed("worktree root")
+                    } else {
+                        Cow::Owned(format!("`{}`", path))
+                    }
+                };
+                let placeholder_text =
+                    format!("Select a {} for {pretty_path}…", term.to_lowercase(),).into();
+                let _ = this.update_in(cx, move |this, window, cx| {
+                    this.delegate.relative_path = relative_path;
+                    this.delegate.placeholder_text = placeholder_text;
+                    this.refresh_placeholder(window, cx);
+                });
 
                 let _ = this.update_in(cx, move |this, window, cx| {
                     this.delegate.candidates = available_toolchains;