toolchain: Respect currently focused file when querying toolchains (#28875)

Piotr Osiewicz created

Closes #21743


https://github.com/user-attachments/assets/0230f233-58a4-494c-90af-28ce82f9fc1d


Release Notes:

- Virtual environment picker now looks up virtual environment based on
parent directory of active file; this enables having multiple active
virtual environments in a single worktree.

Change summary

crates/project/src/project.rs                       |  3 
crates/project/src/toolchain_store.rs               |  3 
crates/title_bar/src/title_bar.rs                   |  2 
crates/toolchain_selector/src/active_toolchain.rs   | 53 ++++++++++++--
crates/toolchain_selector/src/toolchain_selector.rs | 32 ++++++--
crates/workspace/src/persistence.rs                 |  5 
6 files changed, 77 insertions(+), 21 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -3094,6 +3094,9 @@ impl Project {
             .map(|lister| lister.term())
     }
 
+    pub fn toolchain_store(&self) -> Option<Entity<ToolchainStore>> {
+        self.toolchain_store.clone()
+    }
     pub fn activate_toolchain(
         &self,
         path: ProjectPath,

crates/project/src/toolchain_store.rs 🔗

@@ -55,6 +55,7 @@ impl ToolchainStore {
         });
         Self(ToolchainStoreInner::Local(entity, subscription))
     }
+
     pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self {
         Self(ToolchainStoreInner::Remote(
             cx.new(|_| RemoteToolchainStore { client, project_id }),
@@ -285,7 +286,7 @@ struct LocalStore(WeakEntity<LocalToolchainStore>);
 struct RemoteStore(WeakEntity<RemoteToolchainStore>);
 
 #[derive(Clone)]
-pub(crate) enum ToolchainStoreEvent {
+pub enum ToolchainStoreEvent {
     ToolchainActivated,
 }
 

crates/title_bar/src/title_bar.rs 🔗

@@ -302,7 +302,7 @@ impl TitleBar {
                 cx.notify()
             }),
         );
-        subscriptions.push(cx.subscribe(&project, |_, _, _, cx| cx.notify()));
+        subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
         subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
         subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));

crates/toolchain_selector/src/active_toolchain.rs 🔗

@@ -1,4 +1,4 @@
-use std::sync::Arc;
+use std::{path::Path, sync::Arc};
 
 use editor::Editor;
 use gpui::{
@@ -6,7 +6,7 @@ use gpui::{
     WeakEntity, Window, div,
 };
 use language::{Buffer, BufferEvent, LanguageName, Toolchain};
-use project::{Project, ProjectPath, WorktreeId};
+use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent};
 use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
 use workspace::{StatusItemView, Workspace, item::ItemHandle};
 
@@ -22,6 +22,28 @@ pub struct ActiveToolchain {
 
 impl ActiveToolchain {
     pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
+        if let Some(store) = workspace.project().read(cx).toolchain_store() {
+            cx.subscribe_in(
+                &store,
+                window,
+                |this, _, _: &ToolchainStoreEvent, window, cx| {
+                    let editor = this
+                        .workspace
+                        .update(cx, |workspace, cx| {
+                            workspace
+                                .active_item(cx)
+                                .and_then(|item| item.downcast::<Editor>())
+                        })
+                        .ok()
+                        .flatten();
+                    if let Some(editor) = editor {
+                        this.active_toolchain.take();
+                        this.update_lister(editor, window, cx);
+                    }
+                },
+            )
+            .detach();
+        }
         Self {
             active_toolchain: None,
             active_buffer: None,
@@ -57,12 +79,19 @@ impl ActiveToolchain {
                 this.term = term;
                 cx.notify();
             });
-            let worktree_id = active_file
-                .update(cx, |this, cx| Some(this.file()?.worktree_id(cx)))
+            let (worktree_id, path) = active_file
+                .update(cx, |this, cx| {
+                    this.file().and_then(|file| {
+                        Some((
+                            file.worktree_id(cx),
+                            Arc::<Path>::from(file.path().parent()?),
+                        ))
+                    })
+                })
                 .ok()
                 .flatten()?;
             let toolchain =
-                Self::active_toolchain(workspace, worktree_id, language_name, cx).await?;
+                Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
             let _ = this.update(cx, |this, cx| {
                 this.active_toolchain = Some(toolchain);
 
@@ -101,6 +130,7 @@ impl ActiveToolchain {
     fn active_toolchain(
         workspace: WeakEntity<Workspace>,
         worktree_id: WorktreeId,
+        relative_path: Arc<Path>,
         language_name: LanguageName,
         cx: &mut AsyncWindowContext,
     ) -> Task<Option<Toolchain>> {
@@ -114,7 +144,7 @@ impl ActiveToolchain {
                     this.project().read(cx).active_toolchain(
                         ProjectPath {
                             worktree_id,
-                            path: Arc::from("".as_ref()),
+                            path: relative_path.clone(),
                         },
                         language_name.clone(),
                         cx,
@@ -133,7 +163,7 @@ impl ActiveToolchain {
                         project.read(cx).available_toolchains(
                             ProjectPath {
                                 worktree_id,
-                                path: Arc::from("".as_ref()),
+                                path: relative_path.clone(),
                             },
                             language_name,
                             cx,
@@ -144,7 +174,12 @@ impl ActiveToolchain {
                 if let Some(toolchain) = toolchains.toolchains.first() {
                     // Since we don't have a selected toolchain, pick one for user here.
                     workspace::WORKSPACE_DB
-                        .set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
+                        .set_toolchain(
+                            workspace_id,
+                            worktree_id,
+                            relative_path.to_string_lossy().into_owned(),
+                            toolchain.clone(),
+                        )
                         .await
                         .ok()?;
                     project
@@ -152,7 +187,7 @@ impl ActiveToolchain {
                             this.activate_toolchain(
                                 ProjectPath {
                                     worktree_id,
-                                    path: Arc::from("".as_ref()),
+                                    path: relative_path,
                                 },
                                 toolchain.clone(),
                                 cx,

crates/toolchain_selector/src/toolchain_selector.rs 🔗

@@ -50,6 +50,7 @@ impl ToolchainSelector {
 
         let language_name = buffer.read(cx).language()?.name();
         let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
+        let relative_path: Arc<Path> = Arc::from(buffer.read(cx).file()?.path().parent()?);
         let worktree_root_path = project
             .read(cx)
             .worktree_for_id(worktree_id, cx)?
@@ -58,8 +59,9 @@ impl ToolchainSelector {
         let workspace_id = workspace.database_id()?;
         let weak = workspace.weak_handle();
         cx.spawn_in(window, async move |workspace, cx| {
+            let as_str = relative_path.to_string_lossy().into_owned();
             let active_toolchain = workspace::WORKSPACE_DB
-                .toolchain(workspace_id, worktree_id, language_name.clone())
+                .toolchain(workspace_id, worktree_id, as_str, language_name.clone())
                 .await
                 .ok()
                 .flatten();
@@ -72,6 +74,7 @@ impl ToolchainSelector {
                             active_toolchain,
                             worktree_id,
                             worktree_root_path,
+                            relative_path,
                             language_name,
                             window,
                             cx,
@@ -91,6 +94,7 @@ impl ToolchainSelector {
         active_toolchain: Option<Toolchain>,
         worktree_id: WorktreeId,
         worktree_root: Arc<Path>,
+        relative_path: Arc<Path>,
         language_name: LanguageName,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -104,6 +108,7 @@ impl ToolchainSelector {
                 worktree_id,
                 worktree_root,
                 project,
+                relative_path,
                 language_name,
                 window,
                 cx,
@@ -137,6 +142,7 @@ pub struct ToolchainSelectorDelegate {
     workspace: WeakEntity<Workspace>,
     worktree_id: WorktreeId,
     worktree_abs_path_root: Arc<Path>,
+    relative_path: Arc<Path>,
     placeholder_text: Arc<str>,
     _fetch_candidates_task: Task<Option<()>>,
 }
@@ -149,6 +155,7 @@ impl ToolchainSelectorDelegate {
         worktree_id: WorktreeId,
         worktree_abs_path_root: Arc<Path>,
         project: Entity<Project>,
+        relative_path: Arc<Path>,
         language_name: LanguageName,
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
@@ -162,17 +169,26 @@ impl ToolchainSelectorDelegate {
                     })
                     .ok()?
                     .await?;
-                let placeholder_text = format!("Select a {}…", term.to_lowercase()).into();
+                let relative_path = this
+                    .update(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
                     .update(cx, |this, cx| {
                         this.available_toolchains(
                             ProjectPath {
                                 worktree_id,
-                                path: Arc::from("".as_ref()),
+                                path: relative_path.clone(),
                             },
                             language_name,
                             cx,
@@ -211,6 +227,7 @@ impl ToolchainSelectorDelegate {
             worktree_id,
             worktree_abs_path_root,
             placeholder_text,
+            relative_path,
             _fetch_candidates_task,
         }
     }
@@ -246,19 +263,18 @@ impl PickerDelegate for ToolchainSelectorDelegate {
             {
                 let workspace = self.workspace.clone();
                 let worktree_id = self.worktree_id;
+                let path = self.relative_path.clone();
+                let relative_path = self.relative_path.to_string_lossy().into_owned();
                 cx.spawn_in(window, async move |_, cx| {
                     workspace::WORKSPACE_DB
-                        .set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
+                        .set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
                         .await
                         .log_err();
                     workspace
                         .update(cx, |this, cx| {
                             this.project().update(cx, |this, cx| {
                                 this.activate_toolchain(
-                                    ProjectPath {
-                                        worktree_id,
-                                        path: Arc::from("".as_ref()),
-                                    },
+                                    ProjectPath { worktree_id, path },
                                     toolchain,
                                     cx,
                                 )

crates/workspace/src/persistence.rs 🔗

@@ -1334,17 +1334,18 @@ impl WorkspaceDb {
         &self,
         workspace_id: WorkspaceId,
         worktree_id: WorktreeId,
+        relative_path: String,
         language_name: LanguageName,
     ) -> Result<Option<Toolchain>> {
         self.write(move |this| {
             let mut select = this
                 .select_bound(sql!(
-                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ?
+                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ?
                 ))
                 .context("Preparing insertion")?;
 
             let toolchain: Vec<(String, String, String)> =
-                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize()))?;
+                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?;
 
             Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
                 name: name.into(),