Another lsp tool UI migration (#34009)

Kirill Bulatov created

https://github.com/user-attachments/assets/54182f0d-43e9-4482-89b9-94db5ddaabf8

Release Notes:

- N/A

Change summary

Cargo.lock                                                      |   1 
crates/agent_ui/src/context_picker.rs                           |   2 
crates/inline_completion_button/src/inline_completion_button.rs |   4 
crates/language_tools/Cargo.toml                                |   1 
crates/language_tools/src/lsp_tool.rs                           | 871 +-
crates/ui/src/components/context_menu.rs                        |  25 
crates/ui/src/components/popover_menu.rs                        |  18 
7 files changed, 443 insertions(+), 479 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9023,7 +9023,6 @@ dependencies = [
  "itertools 0.14.0",
  "language",
  "lsp",
- "picker",
  "project",
  "release_channel",
  "serde_json",

crates/agent_ui/src/context_picker.rs 🔗

@@ -426,6 +426,7 @@ impl ContextPicker {
                             this.add_recent_file(project_path.clone(), window, cx);
                         })
                     },
+                    None,
                 )
             }
             RecentEntry::Thread(thread) => {
@@ -443,6 +444,7 @@ impl ContextPicker {
                                 .detach_and_log_err(cx);
                         })
                     },
+                    None,
                 )
             }
         }

crates/language_tools/Cargo.toml 🔗

@@ -24,7 +24,6 @@ gpui.workspace = true
 itertools.workspace = true
 language.workspace = true
 lsp.workspace = true
-picker.workspace = true
 project.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/language_tools/src/lsp_tool.rs 🔗

@@ -1,19 +1,18 @@
-use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration};
+use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration};
 
 use client::proto;
 use collections::{HashMap, HashSet};
 use editor::{Editor, EditorEvent};
 use feature_flags::FeatureFlagAppExt as _;
-use gpui::{
-    Corner, DismissEvent, Entity, Focusable as _, MouseButton, Subscription, Task, WeakEntity,
-    actions,
-};
+use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
 use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
 use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
-use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
 use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
 use settings::{Settings as _, SettingsStore};
-use ui::{Context, Indicator, PopoverMenuHandle, Tooltip, Window, prelude::*};
+use ui::{
+    Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
+    Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
+};
 
 use workspace::{StatusItemView, Workspace};
 
@@ -28,33 +27,38 @@ actions!(
 );
 
 pub struct LspTool {
-    state: Entity<PickerState>,
-    popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
-    lsp_picker: Option<Entity<Picker<LspPickerDelegate>>>,
+    server_state: Entity<LanguageServerState>,
+    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
+    lsp_menu: Option<Entity<ContextMenu>>,
+    lsp_menu_refresh: Task<()>,
     _subscriptions: Vec<Subscription>,
 }
 
-struct PickerState {
+#[derive(Debug)]
+struct LanguageServerState {
+    items: Vec<LspItem>,
+    other_servers_start_index: Option<usize>,
     workspace: WeakEntity<Workspace>,
     lsp_store: WeakEntity<LspStore>,
     active_editor: Option<ActiveEditor>,
     language_servers: LanguageServers,
 }
 
-#[derive(Debug)]
-pub struct LspPickerDelegate {
-    state: Entity<PickerState>,
-    selected_index: usize,
-    items: Vec<LspItem>,
-    other_servers_start_index: Option<usize>,
-}
-
 struct ActiveEditor {
     editor: WeakEntity<Editor>,
     _editor_subscription: Subscription,
     editor_buffers: HashSet<BufferId>,
 }
 
+impl std::fmt::Debug for ActiveEditor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ActiveEditor")
+            .field("editor", &self.editor)
+            .field("editor_buffers", &self.editor_buffers)
+            .finish_non_exhaustive()
+    }
+}
+
 #[derive(Debug, Default, Clone)]
 struct LanguageServers {
     health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
@@ -104,192 +108,154 @@ impl LanguageServerHealthStatus {
     }
 }
 
-impl LspPickerDelegate {
-    fn regenerate_items(&mut self, cx: &mut Context<Picker<Self>>) {
-        self.state.update(cx, |state, cx| {
-            let editor_buffers = 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()
+impl LanguageServerState {
+    fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
+        let lsp_logs = cx
+            .try_global::<GlobalLogStore>()
+            .and_then(|lsp_logs| lsp_logs.0.upgrade());
+        let lsp_store = self.lsp_store.upgrade();
+        let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
+            return menu;
+        };
+
+        for (i, item) in self.items.iter().enumerate() {
+            if let LspItem::ToggleServersButton { restart } = item {
+                let label = if *restart {
+                    "Restart All Servers"
+                } else {
+                    "Stop All Servers"
+                };
+                let restart = *restart;
+                let button = ContextMenuEntry::new(label).handler({
+                    let state = cx.entity();
+                    move |_, cx| {
+                        let lsp_store = state.read(cx).lsp_store.clone();
+                        lsp_store
+                            .update(cx, |lsp_store, cx| {
+                                if restart {
+                                    let Some(workspace) = state.read(cx).workspace.upgrade() else {
+                                        return;
+                                    };
+                                    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)
-                                        .get(*buffer_id)?
+                                        .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| {
+                                            buffer_store.read(cx).get_by_path(&project_path)
+                                        })
+                                        .collect();
+                                    let selectors = state
                                         .read(cx)
-                                        .file(),
-                                )?
-                                .abs_path(cx),
-                            )
-                        })
-                        .ok()??;
-                    Some(buffer_path)
-                })
-                .collect::<Vec<_>>();
-
-            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());
-                        }
+                                        .items
+                                        .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()),
+                                            ),
+                                        })
+                                        .collect();
+                                    lsp_store.restart_language_servers_for_buffers(
+                                        buffers, selectors, cx,
+                                    );
+                                } else {
+                                    lsp_store.stop_all_language_servers(cx);
+                                }
+                            })
+                            .ok();
                     }
-                    acc
                 });
-            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,
-                    ));
-                }
-            }
-
-            let mut can_stop_all = false;
-            let mut can_restart_all = true;
+                menu = menu.separator().item(button);
+                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
+            let has_logs = lsp_store.read(cx).as_local().is_some()
+                && lsp_logs.read(cx).has_server_logs(&server_selector);
+            let status_color = server_info
+                .binary_status
+                .and_then(|binary_status| match binary_status.status {
+                    BinaryStatus::None => None,
+                    BinaryStatus::CheckingForUpdate
+                    | BinaryStatus::Downloading
+                    | BinaryStatus::Starting => Some(Color::Modified),
+                    BinaryStatus::Stopping => Some(Color::Disabled),
+                    BinaryStatus::Stopped => Some(Color::Disabled),
+                    BinaryStatus::Failed { .. } => Some(Color::Error),
+                })
+                .or_else(|| {
+                    Some(match server_info.health? {
+                        ServerHealth::Ok => Color::Success,
+                        ServerHealth::Warning => Color::Warning,
+                        ServerHealth::Error => Color::Error,
+                    })
+                })
+                .unwrap_or(Color::Success);
 
-            for (server_name, status) in state
-                .language_servers
-                .binary_statuses
-                .iter()
-                .filter(|(name, _)| !servers_with_health_checks.contains(name))
+            if self
+                .other_servers_start_index
+                .is_some_and(|index| index == i)
             {
-                match status.status {
-                    BinaryStatus::None => {
-                        can_restart_all = false;
-                        can_stop_all = true;
-                    }
-                    BinaryStatus::CheckingForUpdate => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Downloading => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Starting => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Stopping => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Stopped => {}
-                    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
-                        }
-                    });
-                if let Some(server_id) = matching_server_id {
-                    buffer_servers.push(ServerData::WithBinaryStatus(
-                        Some(server_id),
-                        server_name,
-                        status,
-                    ));
-                } else {
-                    other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
-                }
+                menu = menu.separator();
             }
-
-            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());
-            }
-            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: false });
-                } else if can_restart_all {
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
-                }
-            }
-
-            self.items = new_lsp_items;
-            self.other_servers_start_index = other_servers_start_index;
-        });
-    }
-
-    fn server_info(&self, ix: usize) -> Option<ServerInfo> {
-        match self.items.get(ix)? {
-            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(),
-            }),
-            LspItem::WithBinaryStatus(
-                server_id,
-                language_server_name,
-                language_server_binary_status,
-            ) => Some(ServerInfo {
-                name: language_server_name.clone(),
-                id: *server_id,
-                health: None,
-                binary_status: Some(language_server_binary_status.clone()),
-                message: language_server_binary_status.message.clone(),
-            }),
+            menu = menu.item(ContextMenuItem::custom_entry(
+                move |_, _| {
+                    h_flex()
+                        .gap_1()
+                        .w_full()
+                        .child(Indicator::dot().color(status_color))
+                        .child(Label::new(server_info.name.0.clone()))
+                        .when(!has_logs, |div| div.cursor_default())
+                        .into_any_element()
+                },
+                {
+                    let lsp_logs = lsp_logs.clone();
+                    move |window, cx| {
+                        if !has_logs {
+                            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| {
+                    DocumentationAside::new(
+                        DocumentationSide::Right,
+                        Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
+                    )
+                }),
+            ));
         }
+        menu
     }
 }
 
@@ -375,6 +341,36 @@ enum LspItem {
     },
 }
 
+impl LspItem {
+    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(),
+            }),
+            LspItem::WithBinaryStatus(
+                server_id,
+                language_server_name,
+                language_server_binary_status,
+            ) => Some(ServerInfo {
+                name: language_server_name.clone(),
+                id: *server_id,
+                health: None,
+                binary_status: Some(language_server_binary_status.clone()),
+                message: language_server_binary_status.message.clone(),
+            }),
+        }
+    }
+}
+
 impl ServerData<'_> {
     fn name(&self) -> &LanguageServerName {
         match self {
@@ -395,267 +391,21 @@ impl ServerData<'_> {
     }
 }
 
-impl PickerDelegate for LspPickerDelegate {
-    type ListItem = AnyElement;
-
-    fn match_count(&self) -> usize {
-        self.items.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-        self.selected_index = ix;
-        cx.notify();
-    }
-
-    fn update_matches(
-        &mut self,
-        _: String,
-        _: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        cx.spawn(async move |lsp_picker, cx| {
-            cx.background_executor()
-                .timer(Duration::from_millis(30))
-                .await;
-            lsp_picker
-                .update(cx, |lsp_picker, cx| {
-                    lsp_picker.delegate.regenerate_items(cx);
-                })
-                .ok();
-        })
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        Arc::default()
-    }
-
-    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(self.selected_index)
-        {
-            let lsp_store = self.state.read(cx).lsp_store.clone();
-            lsp_store
-                .update(cx, |lsp_store, cx| {
-                    if *restart {
-                        let Some(workspace) = self.state.read(cx).workspace.upgrade() else {
-                            return;
-                        };
-                        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 = self
-                            .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| {
-                                buffer_store.read(cx).get_by_path(&project_path)
-                            })
-                            .collect();
-                        let selectors = self
-                            .items
-                            .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()))
-                                }
-                            })
-                            .collect();
-                        lsp_store.restart_language_servers_for_buffers(buffers, selectors, cx);
-                    } else {
-                        lsp_store.stop_all_language_servers(cx);
-                    }
-                })
-                .ok();
-        }
-
-        let Some(server_selector) = self
-            .server_info(self.selected_index)
-            .map(|info| info.server_selector())
-        else {
-            return;
-        };
-        let lsp_logs = cx.global::<GlobalLogStore>().0.clone();
-        let lsp_store = self.state.read(cx).lsp_store.clone();
-        let workspace = self.state.read(cx).workspace.clone();
-        lsp_logs
-            .update(cx, |lsp_logs, cx| {
-                let has_logs = lsp_store
-                    .update(cx, |lsp_store, _| {
-                        lsp_store.as_local().is_some() && lsp_logs.has_server_logs(&server_selector)
-                    })
-                    .unwrap_or(false);
-                if has_logs {
-                    lsp_logs.open_server_trace(workspace, server_selector, window, cx);
-                }
-            })
-            .ok();
-    }
-
-    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-        cx.emit(DismissEvent);
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let rendered_match = h_flex().px_1().gap_1();
-        let rendered_match_contents = h_flex()
-            .id(("lsp-item", ix))
-            .w_full()
-            .px_2()
-            .gap_2()
-            .when(selected, |server_entry| {
-                server_entry.bg(cx.theme().colors().element_hover)
-            })
-            .hover(|s| s.bg(cx.theme().colors().element_hover));
-
-        if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(ix) {
-            let label = Label::new(if *restart {
-                "Restart All Servers"
-            } else {
-                "Stop All Servers"
-            });
-            return Some(
-                rendered_match
-                    .child(rendered_match_contents.child(label))
-                    .into_any_element(),
-            );
-        }
-
-        let server_info = self.server_info(ix)?;
-        let workspace = self.state.read(cx).workspace.clone();
-        let lsp_logs = cx.global::<GlobalLogStore>().0.upgrade()?;
-        let lsp_store = self.state.read(cx).lsp_store.upgrade()?;
-        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
-        let has_logs = lsp_store.read(cx).as_local().is_some()
-            && lsp_logs.read(cx).has_server_logs(&server_selector);
-
-        let status_color = server_info
-            .binary_status
-            .and_then(|binary_status| match binary_status.status {
-                BinaryStatus::None => None,
-                BinaryStatus::CheckingForUpdate
-                | BinaryStatus::Downloading
-                | BinaryStatus::Starting => Some(Color::Modified),
-                BinaryStatus::Stopping => Some(Color::Disabled),
-                BinaryStatus::Stopped => Some(Color::Disabled),
-                BinaryStatus::Failed { .. } => Some(Color::Error),
-            })
-            .or_else(|| {
-                Some(match server_info.health? {
-                    ServerHealth::Ok => Color::Success,
-                    ServerHealth::Warning => Color::Warning,
-                    ServerHealth::Error => Color::Error,
-                })
-            })
-            .unwrap_or(Color::Success);
-
-        Some(
-            rendered_match
-                .child(
-                    rendered_match_contents
-                        .child(Indicator::dot().color(status_color))
-                        .child(Label::new(server_info.name.0.clone()))
-                        .when_some(
-                            server_info.message.clone(),
-                            |server_entry, server_message| {
-                                server_entry.tooltip(Tooltip::text(server_message.clone()))
-                            },
-                        ),
-                )
-                .when_else(
-                    has_logs,
-                    |server_entry| {
-                        server_entry.on_mouse_down(MouseButton::Left, {
-                            let workspace = workspace.clone();
-                            let lsp_logs = lsp_logs.downgrade();
-                            let server_selector = server_selector.clone();
-                            move |_, window, cx| {
-                                lsp_logs
-                                    .update(cx, |lsp_logs, cx| {
-                                        lsp_logs.open_server_trace(
-                                            workspace.clone(),
-                                            server_selector.clone(),
-                                            window,
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            }
-                        })
-                    },
-                    |div| div.cursor_default(),
-                )
-                .into_any_element(),
-        )
-    }
-
-    fn render_editor(
-        &self,
-        editor: &Entity<Editor>,
-        _: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Div {
-        div().child(div().track_focus(&editor.focus_handle(cx)))
-    }
-
-    fn separators_after_indices(&self) -> Vec<usize> {
-        if self.items.is_empty() {
-            return Vec::new();
-        }
-        let mut indices = vec![self.items.len().saturating_sub(2)];
-        if let Some(other_servers_start_index) = self.other_servers_start_index {
-            if other_servers_start_index > 0 {
-                indices.insert(0, other_servers_start_index - 1);
-                indices.dedup();
-            }
-        }
-        indices
-    }
-}
-
 impl LspTool {
     pub fn new(
         workspace: &Workspace,
-        popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
+        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         let settings_subscription =
             cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
                 if ProjectSettings::get_global(cx).global_lsp_settings.button {
-                    if lsp_tool.lsp_picker.is_none() {
-                        lsp_tool.lsp_picker =
-                            Some(Self::new_lsp_picker(lsp_tool.state.clone(), window, cx));
-                        cx.notify();
+                    if lsp_tool.lsp_menu.is_none() {
+                        lsp_tool.refresh_lsp_menu(true, window, cx);
                         return;
                     }
-                } else if lsp_tool.lsp_picker.take().is_some() {
+                } else if lsp_tool.lsp_menu.take().is_some() {
                     cx.notify();
                 }
             });
@@ -666,17 +416,20 @@ impl LspTool {
                 lsp_tool.on_lsp_store_event(e, window, cx)
             });
 
-        let state = cx.new(|_| PickerState {
+        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(),
         });
 
         Self {
-            state,
+            server_state: state,
             popover_menu_handle,
-            lsp_picker: None,
+            lsp_menu: None,
+            lsp_menu_refresh: Task::ready(()),
             _subscriptions: vec![settings_subscription, lsp_store_subscription],
         }
     }
@@ -687,7 +440,7 @@ impl LspTool {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(lsp_picker) = self.lsp_picker.clone() else {
+        if self.lsp_menu.is_none() {
             return;
         };
         let mut updated = false;
@@ -720,7 +473,7 @@ impl LspTool {
                                 BinaryStatus::Failed { error }
                             }
                         };
-                        self.state.update(cx, |state, _| {
+                        self.server_state.update(cx, |state, _| {
                             state.language_servers.update_binary_status(
                                 binary_status,
                                 status_update.message.as_deref(),
@@ -737,7 +490,7 @@ impl LspTool {
                             proto::ServerHealth::Warning => ServerHealth::Warning,
                             proto::ServerHealth::Error => ServerHealth::Error,
                         };
-                        self.state.update(cx, |state, _| {
+                        self.server_state.update(cx, |state, _| {
                             state.language_servers.update_server_health(
                                 *language_server_id,
                                 health,
@@ -756,7 +509,7 @@ impl LspTool {
                 message: proto::update_language_server::Variant::RegisteredForBuffer(update),
                 ..
             } => {
-                self.state.update(cx, |state, _| {
+                self.server_state.update(cx, |state, _| {
                     state
                         .language_servers
                         .servers_per_buffer_abs_path
@@ -770,27 +523,203 @@ impl LspTool {
         };
 
         if updated {
-            lsp_picker.update(cx, |lsp_picker, cx| {
-                lsp_picker.refresh(window, cx);
-            });
+            self.refresh_lsp_menu(false, window, cx);
         }
     }
 
-    fn new_lsp_picker(
-        state: Entity<PickerState>,
+    fn regenerate_items(&mut self, cx: &mut App) {
+        self.server_state.update(cx, |state, cx| {
+            let editor_buffers = 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),
+                            )
+                        })
+                        .ok()??;
+                    Some(buffer_path)
+                })
+                .collect::<Vec<_>>();
+
+            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());
+                        }
+                    }
+                    acc
+                });
+            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,
+                    ));
+                }
+            }
+
+            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
+                .language_servers
+                .binary_statuses
+                .iter()
+                .filter(|(name, _)| !servers_with_health_checks.contains(name))
+            {
+                match status.status {
+                    BinaryStatus::None => {
+                        can_restart_all = false;
+                        can_stop_all |= true;
+                    }
+                    BinaryStatus::CheckingForUpdate => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Downloading => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Starting => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Stopping => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Stopped => {}
+                    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
+                        }
+                    });
+                if let Some(server_id) = matching_server_id {
+                    buffer_servers.push(ServerData::WithBinaryStatus(
+                        Some(server_id),
+                        server_name,
+                        status,
+                    ));
+                } else {
+                    other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
+                }
+            }
+
+            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());
+            }
+            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: false });
+                } else if can_restart_all {
+                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
+                }
+            }
+
+            state.items = new_lsp_items;
+            state.other_servers_start_index = other_servers_start_index;
+        });
+    }
+
+    fn refresh_lsp_menu(
+        &mut self,
+        create_if_empty: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Entity<Picker<LspPickerDelegate>> {
-        cx.new(|cx| {
-            let mut delegate = LspPickerDelegate {
-                selected_index: 0,
-                other_servers_start_index: None,
-                items: Vec::new(),
-                state,
-            };
-            delegate.regenerate_items(cx);
-            Picker::list(delegate, window, cx)
-        })
+    ) {
+        if create_if_empty || self.lsp_menu.is_some() {
+            let state = self.server_state.clone();
+            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| {
+                cx.background_executor()
+                    .timer(Duration::from_millis(30))
+                    .await;
+                lsp_tool
+                    .update_in(cx, |lsp_tool, window, cx| {
+                        lsp_tool.regenerate_items(cx);
+                        let menu = ContextMenu::build(window, cx, |menu, _, cx| {
+                            state.update(cx, |state, cx| state.fill_menu(menu, cx))
+                        });
+                        lsp_tool.lsp_menu = Some(menu.clone());
+                        // TODO kb will this work?
+                        // what about the selections?
+                        lsp_tool.popover_menu_handle.refresh_menu(
+                            window,
+                            cx,
+                            Rc::new(move |_, _| Some(menu.clone())),
+                        );
+                        cx.notify();
+                    })
+                    .ok();
+            });
+        }
     }
 }
 
@@ -805,7 +734,7 @@ impl StatusItemView for LspTool {
             if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
                 if Some(&editor)
                     != self
-                        .state
+                        .server_state
                         .read(cx)
                         .active_editor
                         .as_ref()

crates/ui/src/components/context_menu.rs 🔗

@@ -24,6 +24,7 @@ pub enum ContextMenuItem {
         entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
         handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
         selectable: bool,
+        documentation_aside: Option<DocumentationAside>,
     },
 }
 
@@ -31,11 +32,13 @@ impl ContextMenuItem {
     pub fn custom_entry(
         entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
         handler: impl Fn(&mut Window, &mut App) + 'static,
+        documentation_aside: Option<DocumentationAside>,
     ) -> Self {
         Self::CustomEntry {
             entry_render: Box::new(entry_render),
             handler: Rc::new(move |_, window, cx| handler(window, cx)),
             selectable: true,
+            documentation_aside,
         }
     }
 }
@@ -170,6 +173,12 @@ pub struct DocumentationAside {
     render: Rc<dyn Fn(&mut App) -> AnyElement>,
 }
 
+impl DocumentationAside {
+    pub fn new(side: DocumentationSide, render: Rc<dyn Fn(&mut App) -> AnyElement>) -> Self {
+        Self { side, render }
+    }
+}
+
 impl Focusable for ContextMenu {
     fn focus_handle(&self, _cx: &App) -> FocusHandle {
         self.focus_handle.clone()
@@ -456,6 +465,7 @@ impl ContextMenu {
             entry_render: Box::new(entry_render),
             handler: Rc::new(|_, _, _| {}),
             selectable: false,
+            documentation_aside: None,
         });
         self
     }
@@ -469,6 +479,7 @@ impl ContextMenu {
             entry_render: Box::new(entry_render),
             handler: Rc::new(move |_, window, cx| handler(window, cx)),
             selectable: true,
+            documentation_aside: None,
         });
         self
     }
@@ -705,10 +716,19 @@ impl ContextMenu {
         let item = self.items.get(ix)?;
         if item.is_selectable() {
             self.selected_index = Some(ix);
-            if let ContextMenuItem::Entry(entry) = item {
-                if let Some(callback) = &entry.documentation_aside {
+            match item {
+                ContextMenuItem::Entry(entry) => {
+                    if let Some(callback) = &entry.documentation_aside {
+                        self.documentation_aside = Some((ix, callback.clone()));
+                    }
+                }
+                ContextMenuItem::CustomEntry {
+                    documentation_aside: Some(callback),
+                    ..
+                } => {
                     self.documentation_aside = Some((ix, callback.clone()));
                 }
+                _ => (),
             }
         }
         Some(ix)
@@ -806,6 +826,7 @@ impl ContextMenu {
                 entry_render,
                 handler,
                 selectable,
+                ..
             } => {
                 let handler = handler.clone();
                 let menu = cx.entity().downgrade();

crates/ui/src/components/popover_menu.rs 🔗

@@ -105,6 +105,24 @@ impl<M: ManagedView> PopoverMenuHandle<M> {
                 .map_or(false, |model| model.focus_handle(cx).is_focused(window))
         })
     }
+
+    pub fn refresh_menu(
+        &self,
+        window: &mut Window,
+        cx: &mut App,
+        new_menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
+    ) {
+        let show_menu = if let Some(state) = self.0.borrow_mut().as_mut() {
+            state.menu_builder = new_menu_builder;
+            state.menu.borrow().is_some()
+        } else {
+            false
+        };
+
+        if show_menu {
+            self.show(window, cx);
+        }
+    }
 }
 
 pub struct PopoverMenu<M: ManagedView> {