Regroup LSP context menu items by the worktree name (#34838)

Kirill Bulatov created

Also 

* remove the feature gate
* open buffers with an error when no logs are present
* adjust the hover text to indicate that difference

<img width="480" height="380" alt="image"
src="https://github.com/user-attachments/assets/6b2350fc-5121-4b1e-bc22-503d964531a2"
/>

Release Notes:

- N/A

Change summary

Cargo.lock                                          |   1 
crates/activity_indicator/src/activity_indicator.rs |   4 
crates/language_tools/Cargo.toml                    |   1 
crates/language_tools/src/lsp_tool.rs               | 592 +++++++++-----
4 files changed, 357 insertions(+), 241 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9165,7 +9165,6 @@ dependencies = [
  "collections",
  "copilot",
  "editor",
- "feature_flags",
  "futures 0.3.31",
  "gpui",
  "itertools 0.14.0",

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -231,7 +231,6 @@ impl ActivityIndicator {
                 status,
             } => {
                 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
-                let project = project.clone();
                 let status = status.clone();
                 let server_name = server_name.clone();
                 cx.spawn_in(window, async move |workspace, cx| {
@@ -247,8 +246,7 @@ impl ActivityIndicator {
                     workspace.update_in(cx, |workspace, window, cx| {
                         workspace.add_item_to_active_pane(
                             Box::new(cx.new(|cx| {
-                                let mut editor =
-                                    Editor::for_buffer(buffer, Some(project.clone()), window, cx);
+                                let mut editor = Editor::for_buffer(buffer, None, window, cx);
                                 editor.set_read_only(true);
                                 editor
                             })),

crates/language_tools/Cargo.toml 🔗

@@ -18,7 +18,6 @@ client.workspace = true
 collections.workspace = true
 copilot.workspace = true
 editor.workspace = true
-feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true
 itertools.workspace = true

crates/language_tools/src/lsp_tool.rs 🔗

@@ -1,13 +1,17 @@
-use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration};
+use std::{
+    collections::{BTreeMap, HashMap},
+    path::{Path, PathBuf},
+    rc::Rc,
+    time::Duration,
+};
 
 use client::proto;
-use collections::{HashMap, HashSet};
+use collections::HashSet;
 use editor::{Editor, EditorEvent};
-use feature_flags::FeatureFlagAppExt as _;
 use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
-use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
+use language::{BinaryStatus, BufferId, ServerHealth};
 use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
-use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
+use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings};
 use settings::{Settings as _, SettingsStore};
 use ui::{
     Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
@@ -36,8 +40,7 @@ pub struct LspTool {
 
 #[derive(Debug)]
 struct LanguageServerState {
-    items: Vec<LspItem>,
-    other_servers_start_index: Option<usize>,
+    items: Vec<LspMenuItem>,
     workspace: WeakEntity<Workspace>,
     lsp_store: WeakEntity<LspStore>,
     active_editor: Option<ActiveEditor>,
@@ -63,8 +66,13 @@ impl std::fmt::Debug for ActiveEditor {
 struct LanguageServers {
     health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
     binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
-    servers_per_buffer_abs_path:
-        HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>,
+    servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
+}
+
+#[derive(Debug, Clone)]
+struct ServersForPath {
+    servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
+    worktree: Option<WeakEntity<Worktree>>,
 }
 
 #[derive(Debug, Clone)]
@@ -120,8 +128,8 @@ impl LanguageServerState {
         };
 
         let mut first_button_encountered = false;
-        for (i, item) in self.items.iter().enumerate() {
-            if let LspItem::ToggleServersButton { restart } = item {
+        for item in &self.items {
+            if let LspMenuItem::ToggleServersButton { restart } = item {
                 let label = if *restart {
                     "Restart All Servers"
                 } else {
@@ -140,22 +148,19 @@ impl LanguageServerState {
                                     };
                                     let project = workspace.read(cx).project().clone();
                                     let buffer_store = project.read(cx).buffer_store().clone();
-                                    let worktree_store = project.read(cx).worktree_store();
-
                                     let buffers = state
                                         .read(cx)
                                         .language_servers
                                         .servers_per_buffer_abs_path
-                                        .keys()
-                                        .filter_map(|abs_path| {
-                                            worktree_store.read(cx).find_worktree(abs_path, cx)
-                                        })
-                                        .filter_map(|(worktree, relative_path)| {
-                                            let entry =
-                                                worktree.read(cx).entry_for_path(&relative_path)?;
-                                            project.read(cx).path_for_entry(entry.id, cx)
-                                        })
-                                        .filter_map(|project_path| {
+                                        .iter()
+                                        .filter_map(|(abs_path, servers)| {
+                                            let worktree =
+                                                servers.worktree.as_ref()?.upgrade()?.read(cx);
+                                            let relative_path =
+                                                abs_path.strip_prefix(&worktree.abs_path()).ok()?;
+                                            let entry = worktree.entry_for_path(&relative_path)?;
+                                            let project_path =
+                                                project.read(cx).path_for_entry(entry.id, cx)?;
                                             buffer_store.read(cx).get_by_path(&project_path)
                                         })
                                         .collect();
@@ -165,13 +170,16 @@ impl LanguageServerState {
                                         .iter()
                                         // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
                                         .flat_map(|item| match item {
-                                            LspItem::ToggleServersButton { .. } => None,
-                                            LspItem::WithHealthCheck(_, status, ..) => Some(
-                                                LanguageServerSelector::Name(status.name.clone()),
-                                            ),
-                                            LspItem::WithBinaryStatus(_, server_name, ..) => Some(
-                                                LanguageServerSelector::Name(server_name.clone()),
+                                            LspMenuItem::Header { .. } => None,
+                                            LspMenuItem::ToggleServersButton { .. } => None,
+                                            LspMenuItem::WithHealthCheck { health, .. } => Some(
+                                                LanguageServerSelector::Name(health.name.clone()),
                                             ),
+                                            LspMenuItem::WithBinaryStatus {
+                                                server_name, ..
+                                            } => Some(LanguageServerSelector::Name(
+                                                server_name.clone(),
+                                            )),
                                         })
                                         .collect();
                                     lsp_store.restart_language_servers_for_buffers(
@@ -190,13 +198,17 @@ impl LanguageServerState {
                 }
                 menu = menu.item(button);
                 continue;
-            };
+            } else if let LspMenuItem::Header { header, separator } = item {
+                menu = menu
+                    .when(*separator, |menu| menu.separator())
+                    .when_some(header.as_ref(), |menu, header| menu.header(header));
+                continue;
+            }
 
             let Some(server_info) = item.server_info() else {
                 continue;
             };
 
-            let workspace = self.workspace.clone();
             let server_selector = server_info.server_selector();
             // TODO currently, Zed remote does not work well with the LSP logs
             // https://github.com/zed-industries/zed/issues/28557
@@ -205,6 +217,7 @@ impl LanguageServerState {
 
             let status_color = server_info
                 .binary_status
+                .as_ref()
                 .and_then(|binary_status| match binary_status.status {
                     BinaryStatus::None => None,
                     BinaryStatus::CheckingForUpdate
@@ -223,17 +236,20 @@ impl LanguageServerState {
                 })
                 .unwrap_or(Color::Success);
 
-            if self
-                .other_servers_start_index
-                .is_some_and(|index| index == i)
-            {
-                menu = menu.separator().header("Other Buffers");
-            }
-
-            if i == 0 && self.other_servers_start_index.is_some() {
-                menu = menu.header("Current Buffer");
-            }
+            let message = server_info
+                .message
+                .as_ref()
+                .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
+                .cloned();
+            let hover_label = if has_logs {
+                Some("View Logs")
+            } else if message.is_some() {
+                Some("View Message")
+            } else {
+                None
+            };
 
+            let server_name = server_info.name.clone();
             menu = menu.item(ContextMenuItem::custom_entry(
                 move |_, _| {
                     h_flex()
@@ -245,42 +261,99 @@ impl LanguageServerState {
                             h_flex()
                                 .gap_2()
                                 .child(Indicator::dot().color(status_color))
-                                .child(Label::new(server_info.name.0.clone())),
-                        )
-                        .child(
-                            h_flex()
-                                .visible_on_hover("menu_item")
-                                .child(
-                                    Label::new("View Logs")
-                                        .size(LabelSize::Small)
-                                        .color(Color::Muted),
-                                )
-                                .child(
-                                    Icon::new(IconName::ChevronRight)
-                                        .size(IconSize::Small)
-                                        .color(Color::Muted),
-                                ),
+                                .child(Label::new(server_name.0.clone())),
                         )
+                        .when_some(hover_label, |div, hover_label| {
+                            div.child(
+                                h_flex()
+                                    .visible_on_hover("menu_item")
+                                    .child(
+                                        Label::new(hover_label)
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted),
+                                    )
+                                    .child(
+                                        Icon::new(IconName::ChevronRight)
+                                            .size(IconSize::Small)
+                                            .color(Color::Muted),
+                                    ),
+                            )
+                        })
                         .into_any_element()
                 },
                 {
                     let lsp_logs = lsp_logs.clone();
+                    let message = message.clone();
+                    let server_selector = server_selector.clone();
+                    let server_name = server_info.name.clone();
+                    let workspace = self.workspace.clone();
                     move |window, cx| {
-                        if !has_logs {
+                        if has_logs {
+                            lsp_logs.update(cx, |lsp_logs, cx| {
+                                lsp_logs.open_server_trace(
+                                    workspace.clone(),
+                                    server_selector.clone(),
+                                    window,
+                                    cx,
+                                );
+                            });
+                        } else if let Some(message) = &message {
+                            let Some(create_buffer) = workspace
+                                .update(cx, |workspace, cx| {
+                                    workspace
+                                        .project()
+                                        .update(cx, |project, cx| project.create_buffer(cx))
+                                })
+                                .ok()
+                            else {
+                                return;
+                            };
+
+                            let window = window.window_handle();
+                            let workspace = workspace.clone();
+                            let message = message.clone();
+                            let server_name = server_name.clone();
+                            cx.spawn(async move |cx| {
+                                let buffer = create_buffer.await?;
+                                buffer.update(cx, |buffer, cx| {
+                                    buffer.edit(
+                                        [(
+                                            0..0,
+                                            format!("Language server {server_name}:\n\n{message}"),
+                                        )],
+                                        None,
+                                        cx,
+                                    );
+                                    buffer.set_capability(language::Capability::ReadOnly, cx);
+                                })?;
+
+                                workspace.update(cx, |workspace, cx| {
+                                    window.update(cx, |_, window, cx| {
+                                        workspace.add_item_to_active_pane(
+                                            Box::new(cx.new(|cx| {
+                                                let mut editor =
+                                                    Editor::for_buffer(buffer, None, window, cx);
+                                                editor.set_read_only(true);
+                                                editor
+                                            })),
+                                            None,
+                                            true,
+                                            window,
+                                            cx,
+                                        );
+                                    })
+                                })??;
+
+                                anyhow::Ok(())
+                            })
+                            .detach();
+                        } else {
                             cx.propagate();
                             return;
                         }
-                        lsp_logs.update(cx, |lsp_logs, cx| {
-                            lsp_logs.open_server_trace(
-                                workspace.clone(),
-                                server_selector.clone(),
-                                window,
-                                cx,
-                            );
-                        });
                     }
                 },
-                server_info.message.map(|server_message| {
+                message.map(|server_message| {
                     DocumentationAside::new(
                         DocumentationSide::Right,
                         Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
@@ -345,81 +418,95 @@ impl LanguageServers {
 
 #[derive(Debug)]
 enum ServerData<'a> {
-    WithHealthCheck(
-        LanguageServerId,
-        &'a LanguageServerHealthStatus,
-        Option<&'a LanguageServerBinaryStatus>,
-    ),
-    WithBinaryStatus(
-        Option<LanguageServerId>,
-        &'a LanguageServerName,
-        &'a LanguageServerBinaryStatus,
-    ),
+    WithHealthCheck {
+        server_id: LanguageServerId,
+        health: &'a LanguageServerHealthStatus,
+        binary_status: Option<&'a LanguageServerBinaryStatus>,
+    },
+    WithBinaryStatus {
+        server_id: Option<LanguageServerId>,
+        server_name: &'a LanguageServerName,
+        binary_status: &'a LanguageServerBinaryStatus,
+    },
 }
 
 #[derive(Debug)]
-enum LspItem {
-    WithHealthCheck(
-        LanguageServerId,
-        LanguageServerHealthStatus,
-        Option<LanguageServerBinaryStatus>,
-    ),
-    WithBinaryStatus(
-        Option<LanguageServerId>,
-        LanguageServerName,
-        LanguageServerBinaryStatus,
-    ),
+enum LspMenuItem {
+    WithHealthCheck {
+        server_id: LanguageServerId,
+        health: LanguageServerHealthStatus,
+        binary_status: Option<LanguageServerBinaryStatus>,
+    },
+    WithBinaryStatus {
+        server_id: Option<LanguageServerId>,
+        server_name: LanguageServerName,
+        binary_status: LanguageServerBinaryStatus,
+    },
     ToggleServersButton {
         restart: bool,
     },
+    Header {
+        header: Option<SharedString>,
+        separator: bool,
+    },
 }
 
-impl LspItem {
+impl LspMenuItem {
     fn server_info(&self) -> Option<ServerInfo> {
         match self {
-            LspItem::ToggleServersButton { .. } => None,
-            LspItem::WithHealthCheck(
-                language_server_id,
-                language_server_health_status,
-                language_server_binary_status,
-            ) => Some(ServerInfo {
-                name: language_server_health_status.name.clone(),
-                id: Some(*language_server_id),
-                health: language_server_health_status.health(),
-                binary_status: language_server_binary_status.clone(),
-                message: language_server_health_status.message(),
+            Self::Header { .. } => None,
+            Self::ToggleServersButton { .. } => None,
+            Self::WithHealthCheck {
+                server_id,
+                health,
+                binary_status,
+                ..
+            } => Some(ServerInfo {
+                name: health.name.clone(),
+                id: Some(*server_id),
+                health: health.health(),
+                binary_status: binary_status.clone(),
+                message: health.message(),
             }),
-            LspItem::WithBinaryStatus(
+            Self::WithBinaryStatus {
                 server_id,
-                language_server_name,
-                language_server_binary_status,
-            ) => Some(ServerInfo {
-                name: language_server_name.clone(),
+                server_name,
+                binary_status,
+                ..
+            } => Some(ServerInfo {
+                name: server_name.clone(),
                 id: *server_id,
                 health: None,
-                binary_status: Some(language_server_binary_status.clone()),
-                message: language_server_binary_status.message.clone(),
+                binary_status: Some(binary_status.clone()),
+                message: binary_status.message.clone(),
             }),
         }
     }
 }
 
 impl ServerData<'_> {
-    fn name(&self) -> &LanguageServerName {
-        match self {
-            Self::WithHealthCheck(_, state, _) => &state.name,
-            Self::WithBinaryStatus(_, name, ..) => name,
-        }
-    }
-
-    fn into_lsp_item(self) -> LspItem {
+    fn into_lsp_item(self) -> LspMenuItem {
         match self {
-            Self::WithHealthCheck(id, name, status) => {
-                LspItem::WithHealthCheck(id, name.clone(), status.cloned())
-            }
-            Self::WithBinaryStatus(server_id, name, status) => {
-                LspItem::WithBinaryStatus(server_id, name.clone(), status.clone())
-            }
+            Self::WithHealthCheck {
+                server_id,
+                health,
+                binary_status,
+                ..
+            } => LspMenuItem::WithHealthCheck {
+                server_id,
+                health: health.clone(),
+                binary_status: binary_status.cloned(),
+            },
+            Self::WithBinaryStatus {
+                server_id,
+                server_name,
+                binary_status,
+                ..
+            } => LspMenuItem::WithBinaryStatus {
+                server_id,
+                server_name: server_name.clone(),
+                binary_status: binary_status.clone(),
+            },
         }
     }
 }
@@ -452,7 +539,6 @@ impl LspTool {
         let state = cx.new(|_| LanguageServerState {
             workspace: workspace.weak_handle(),
             items: Vec::new(),
-            other_servers_start_index: None,
             lsp_store: lsp_store.downgrade(),
             active_editor: None,
             language_servers: LanguageServers::default(),
@@ -542,13 +628,28 @@ impl LspTool {
                 message: proto::update_language_server::Variant::RegisteredForBuffer(update),
                 ..
             } => {
-                self.server_state.update(cx, |state, _| {
-                    state
+                self.server_state.update(cx, |state, cx| {
+                    let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
+                        workspace
+                            .project()
+                            .read(cx)
+                            .find_worktree(Path::new(&update.buffer_abs_path), cx)
+                            .map(|(worktree, _)| worktree.downgrade())
+                    }) else {
+                        return;
+                    };
+                    let entry = state
                         .language_servers
                         .servers_per_buffer_abs_path
                         .entry(PathBuf::from(&update.buffer_abs_path))
-                        .or_default()
-                        .insert(*language_server_id, name.clone());
+                        .or_insert_with(|| ServersForPath {
+                            servers: HashMap::default(),
+                            worktree: worktree.clone(),
+                        });
+                    entry.servers.insert(*language_server_id, name.clone());
+                    if worktree.is_some() {
+                        entry.worktree = worktree;
+                    }
                 });
                 updated = true;
             }
@@ -562,94 +663,95 @@ impl LspTool {
 
     fn regenerate_items(&mut self, cx: &mut App) {
         self.server_state.update(cx, |state, cx| {
-            let editor_buffers = state
+            let active_worktrees = state
                 .active_editor
                 .as_ref()
-                .map(|active_editor| active_editor.editor_buffers.clone())
-                .unwrap_or_default();
-            let editor_buffer_paths = editor_buffers
-                .iter()
-                .filter_map(|buffer_id| {
-                    let buffer_path = state
-                        .lsp_store
-                        .update(cx, |lsp_store, cx| {
-                            Some(
-                                project::File::from_dyn(
-                                    lsp_store
-                                        .buffer_store()
-                                        .read(cx)
-                                        .get(*buffer_id)?
-                                        .read(cx)
-                                        .file(),
-                                )?
-                                .abs_path(cx),
-                            )
+                .into_iter()
+                .flat_map(|active_editor| {
+                    active_editor
+                        .editor
+                        .upgrade()
+                        .into_iter()
+                        .flat_map(|active_editor| {
+                            active_editor
+                                .read(cx)
+                                .buffer()
+                                .read(cx)
+                                .all_buffers()
+                                .into_iter()
+                                .filter_map(|buffer| {
+                                    project::File::from_dyn(buffer.read(cx).file())
+                                })
+                                .map(|buffer_file| buffer_file.worktree.clone())
                         })
-                        .ok()??;
-                    Some(buffer_path)
                 })
-                .collect::<Vec<_>>();
+                .collect::<HashSet<_>>();
 
-            let mut servers_with_health_checks = HashSet::default();
-            let mut server_ids_with_health_checks = HashSet::default();
-            let mut buffer_servers =
-                Vec::with_capacity(state.language_servers.health_statuses.len());
-            let mut other_servers =
-                Vec::with_capacity(state.language_servers.health_statuses.len());
-            let buffer_server_ids = editor_buffer_paths
-                .iter()
-                .filter_map(|buffer_path| {
-                    state
-                        .language_servers
-                        .servers_per_buffer_abs_path
-                        .get(buffer_path)
-                })
-                .flatten()
-                .fold(HashMap::default(), |mut acc, (server_id, name)| {
-                    match acc.entry(*server_id) {
-                        hash_map::Entry::Occupied(mut o) => {
-                            let old_name: &mut Option<&LanguageServerName> = o.get_mut();
-                            if old_name.is_none() {
-                                *old_name = name.as_ref();
-                            }
-                        }
-                        hash_map::Entry::Vacant(v) => {
-                            v.insert(name.as_ref());
+            let mut server_ids_to_worktrees =
+                HashMap::<LanguageServerId, Entity<Worktree>>::default();
+            let mut server_names_to_worktrees = HashMap::<
+                LanguageServerName,
+                HashSet<(Entity<Worktree>, LanguageServerId)>,
+            >::default();
+            for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
+                if let Some(worktree) = servers_for_path
+                    .worktree
+                    .as_ref()
+                    .and_then(|worktree| worktree.upgrade())
+                {
+                    for (server_id, server_name) in &servers_for_path.servers {
+                        server_ids_to_worktrees.insert(*server_id, worktree.clone());
+                        if let Some(server_name) = server_name {
+                            server_names_to_worktrees
+                                .entry(server_name.clone())
+                                .or_default()
+                                .insert((worktree.clone(), *server_id));
                         }
                     }
-                    acc
+                }
+            }
+
+            let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
+            let mut servers_without_worktree = Vec::<ServerData>::new();
+            let mut servers_with_health_checks = HashSet::default();
+
+            for (server_id, health) in &state.language_servers.health_statuses {
+                let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
+                    let worktrees = server_names_to_worktrees.get(&health.name)?;
+                    worktrees
+                        .iter()
+                        .find(|(worktree, _)| active_worktrees.contains(worktree))
+                        .or_else(|| worktrees.iter().next())
+                        .map(|(worktree, _)| worktree)
                 });
-            for (server_id, server_state) in &state.language_servers.health_statuses {
-                let binary_status = state
-                    .language_servers
-                    .binary_statuses
-                    .get(&server_state.name);
-                servers_with_health_checks.insert(&server_state.name);
-                server_ids_with_health_checks.insert(*server_id);
-                if buffer_server_ids.contains_key(server_id) {
-                    buffer_servers.push(ServerData::WithHealthCheck(
-                        *server_id,
-                        server_state,
-                        binary_status,
-                    ));
-                } else {
-                    other_servers.push(ServerData::WithHealthCheck(
-                        *server_id,
-                        server_state,
-                        binary_status,
-                    ));
+                servers_with_health_checks.insert(&health.name);
+                let worktree_name =
+                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name()));
+
+                let binary_status = state.language_servers.binary_statuses.get(&health.name);
+                let server_data = ServerData::WithHealthCheck {
+                    server_id: *server_id,
+                    health,
+                    binary_status,
+                };
+                match worktree_name {
+                    Some(worktree_name) => servers_per_worktree
+                        .entry(worktree_name.clone())
+                        .or_default()
+                        .push(server_data),
+                    None => servers_without_worktree.push(server_data),
                 }
             }
 
             let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
             let mut can_restart_all = state.language_servers.health_statuses.is_empty();
-            for (server_name, status) in state
+            for (server_name, binary_status) in state
                 .language_servers
                 .binary_statuses
                 .iter()
                 .filter(|(name, _)| !servers_with_health_checks.contains(name))
             {
-                match status.status {
+                match binary_status.status {
                     BinaryStatus::None => {
                         can_restart_all = false;
                         can_stop_all |= true;
@@ -674,52 +776,73 @@ impl LspTool {
                     BinaryStatus::Failed { .. } => {}
                 }
 
-                let matching_server_id = state
-                    .language_servers
-                    .servers_per_buffer_abs_path
-                    .iter()
-                    .filter(|(path, _)| editor_buffer_paths.contains(path))
-                    .flat_map(|(_, server_associations)| server_associations.iter())
-                    .find_map(|(id, name)| {
-                        if name.as_ref() == Some(server_name) {
-                            Some(*id)
-                        } else {
-                            None
+                match server_names_to_worktrees.get(server_name) {
+                    Some(worktrees_for_name) => {
+                        match worktrees_for_name
+                            .iter()
+                            .find(|(worktree, _)| active_worktrees.contains(worktree))
+                            .or_else(|| worktrees_for_name.iter().next())
+                        {
+                            Some((worktree, server_id)) => {
+                                let worktree_name =
+                                    SharedString::new(worktree.read(cx).root_name());
+                                servers_per_worktree
+                                    .entry(worktree_name.clone())
+                                    .or_default()
+                                    .push(ServerData::WithBinaryStatus {
+                                        server_name,
+                                        binary_status,
+                                        server_id: Some(*server_id),
+                                    });
+                            }
+                            None => servers_without_worktree.push(ServerData::WithBinaryStatus {
+                                server_name,
+                                binary_status,
+                                server_id: None,
+                            }),
                         }
-                    });
-                if let Some(server_id) = matching_server_id {
-                    buffer_servers.push(ServerData::WithBinaryStatus(
-                        Some(server_id),
+                    }
+                    None => servers_without_worktree.push(ServerData::WithBinaryStatus {
                         server_name,
-                        status,
-                    ));
-                } else {
-                    other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
+                        binary_status,
+                        server_id: None,
+                    }),
                 }
             }
 
-            buffer_servers.sort_by_key(|data| data.name().clone());
-            other_servers.sort_by_key(|data| data.name().clone());
-
-            let mut other_servers_start_index = None;
             let mut new_lsp_items =
-                Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
-            new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
-            if !new_lsp_items.is_empty() {
-                other_servers_start_index = Some(new_lsp_items.len());
+                Vec::with_capacity(servers_per_worktree.len() + servers_without_worktree.len() + 2);
+            for (worktree_name, worktree_servers) in servers_per_worktree {
+                if worktree_servers.is_empty() {
+                    continue;
+                }
+                new_lsp_items.push(LspMenuItem::Header {
+                    header: Some(worktree_name),
+                    separator: false,
+                });
+                new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
+            }
+            if !servers_without_worktree.is_empty() {
+                new_lsp_items.push(LspMenuItem::Header {
+                    header: Some(SharedString::from("Unknown worktree")),
+                    separator: false,
+                });
+                new_lsp_items.extend(
+                    servers_without_worktree
+                        .into_iter()
+                        .map(ServerData::into_lsp_item),
+                );
             }
-            new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
             if !new_lsp_items.is_empty() {
                 if can_stop_all {
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
+                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
+                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
                 } else if can_restart_all {
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
+                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
                 }
             }
 
             state.items = new_lsp_items;
-            state.other_servers_start_index = other_servers_start_index;
         });
     }
 
@@ -841,10 +964,7 @@ impl StatusItemView for LspTool {
 
 impl Render for LspTool {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
-        if !cx.is_staff()
-            || self.server_state.read(cx).language_servers.is_empty()
-            || self.lsp_menu.is_none()
-        {
+        if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
             return div();
         }
 
@@ -852,12 +972,12 @@ impl Render for LspTool {
         let mut has_warnings = false;
         let mut has_other_notifications = false;
         let state = self.server_state.read(cx);
-        for server in state.language_servers.health_statuses.values() {
-            if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) {
-                has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
-                has_other_notifications |= binary_status.message.is_some();
-            }
+        for binary_status in state.language_servers.binary_statuses.values() {
+            has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
+            has_other_notifications |= binary_status.message.is_some();
+        }
 
+        for server in state.language_servers.health_statuses.values() {
             if let Some((message, health)) = &server.health {
                 has_other_notifications |= message.is_some();
                 match health {