Cargo.lock 🔗
@@ -9023,7 +9023,6 @@ dependencies = [
"itertools 0.14.0",
"language",
"lsp",
- "picker",
"project",
"release_channel",
"serde_json",
Kirill Bulatov created
https://github.com/user-attachments/assets/54182f0d-43e9-4482-89b9-94db5ddaabf8
Release Notes:
- N/A
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(-)
@@ -9023,7 +9023,6 @@ dependencies = [
"itertools 0.14.0",
"language",
"lsp",
- "picker",
"project",
"release_channel",
"serde_json",
@@ -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,
)
}
}
@@ -835,10 +835,6 @@ impl InlineCompletionButton {
cx.notify();
}
-
- pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.popover_menu_handle.toggle(window, cx);
- }
}
impl StatusItemView for InlineCompletionButton {
@@ -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
@@ -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()
@@ -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();
@@ -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> {