Show language server path in language server menu (#47037)

Joseph T. Lyons created

<img width="779" height="210" alt="SCR-20260116-qrif"
src="https://github.com/user-attachments/assets/13c84f20-8fe1-4e3a-9de6-8df1024c2c6c"
/>

In most languages, Zed will check 2-3 locations for language servers.
For instance, when opening a python file, zed looks for ruff in:

- project venv
- user's PATH
- location that zed installs language servers

The language server menu surfaces information that makes it easier to
gather data about your language server, I think offering a path for the
one being ran makes things more clear to the user.

That being said, Idk the best way to surface this. (@danilo-leal?)

Release Notes:

- Add language server path to a tooltip that is produced when hovering
on its status in the language server menu

Change summary

crates/language_tools/src/lsp_button.rs | 29 ++++++++++++++++++++++----
1 file changed, 24 insertions(+), 5 deletions(-)

Detailed changes

crates/language_tools/src/lsp_button.rs 🔗

@@ -20,7 +20,7 @@ use ui::{
     ContextMenu, ContextMenuEntry, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
 };
 
-use util::{ResultExt, rel_path::RelPath};
+use util::{ResultExt, paths::PathExt, rel_path::RelPath};
 use workspace::{StatusItemView, Workspace};
 
 use crate::lsp_log_view;
@@ -132,12 +132,20 @@ impl LanguageServerState {
             return menu;
         };
 
-        let server_versions = self
+        let server_metadata = self
             .lsp_store
             .update(cx, |lsp_store, _| {
                 lsp_store
                     .language_server_statuses()
-                    .map(|(server_id, status)| (server_id, status.server_version.clone()))
+                    .map(|(server_id, status)| {
+                        (
+                            server_id,
+                            (
+                                status.server_version.clone(),
+                                status.binary.as_ref().map(|b| b.path.clone()),
+                            ),
+                        )
+                    })
                     .collect::<HashMap<_, _>>()
             })
             .unwrap_or_default();
@@ -266,9 +274,16 @@ impl LanguageServerState {
                 .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
                 .cloned();
 
-            let server_version = server_versions
+            let (server_version, binary_path) = server_metadata
                 .get(&server_info.id)
-                .and_then(|version| version.clone());
+                .map(|(version, path)| {
+                    (
+                        version.clone(),
+                        path.as_ref()
+                            .map(|p| SharedString::from(p.compact().to_string_lossy().to_string())),
+                    )
+                })
+                .unwrap_or((None, None));
 
             let truncated_message = message.as_ref().and_then(|message| {
                 message
@@ -495,6 +510,7 @@ impl LanguageServerState {
 
                         submenu = submenu.separator().custom_row({
                             let metadata_label = metadata_label.clone();
+                            let binary_path = binary_path.clone();
                             move |_, _| {
                                 h_flex()
                                     .id("metadata-container")
@@ -525,6 +541,9 @@ impl LanguageServerState {
                                                     .truncate(),
                                             )
                                     })
+                                    .when_some(binary_path.clone(), |el, path| {
+                                        el.tooltip(Tooltip::text(path))
+                                    })
                                     .into_any_element()
                             }
                         });