diff --git a/Cargo.lock b/Cargo.lock index baed77a49f..921eea00f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9023,7 +9023,6 @@ dependencies = [ "itertools 0.14.0", "language", "lsp", - "picker", "project", "release_channel", "serde_json", diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index f303f34a52..73fc0b36ce 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/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, ) } } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index f8123d676a..7e6b77b93d 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -835,10 +835,6 @@ impl InlineCompletionButton { cx.notify(); } - - pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context) { - self.popover_menu_handle.toggle(window, cx); - } } impl StatusItemView for InlineCompletionButton { diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index ffdc939809..45af7518d5 100644 --- a/crates/language_tools/Cargo.toml +++ b/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 diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 24a53ae252..6cd2f83184 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/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, - popover_menu_handle: PopoverMenuHandle>, - lsp_picker: Option>>, + server_state: Entity, + popover_menu_handle: PopoverMenuHandle, + lsp_menu: Option>, + lsp_menu_refresh: Task<()>, _subscriptions: Vec, } -struct PickerState { +#[derive(Debug)] +struct LanguageServerState { + items: Vec, + other_servers_start_index: Option, workspace: WeakEntity, lsp_store: WeakEntity, active_editor: Option, language_servers: LanguageServers, } -#[derive(Debug)] -pub struct LspPickerDelegate { - state: Entity, - selected_index: usize, - items: Vec, - other_servers_start_index: Option, -} - struct ActiveEditor { editor: WeakEntity, _editor_subscription: Subscription, editor_buffers: HashSet, } +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, @@ -104,192 +108,154 @@ impl LanguageServerHealthStatus { } } -impl LspPickerDelegate { - fn regenerate_items(&mut self, cx: &mut Context>) { - 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() - .read(cx) - .get(*buffer_id)? - .read(cx) - .file(), - )? - .abs_path(cx), - ) - }) - .ok()??; - Some(buffer_path) - }) - .collect::>(); +impl LanguageServerState { + fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context) -> ContextMenu { + let lsp_logs = cx + .try_global::() + .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; + }; - 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()); - } + 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) + .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) + .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, - )); - } - } + 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); - let mut can_stop_all = false; - let mut can_restart_all = true; - - 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 + menu = menu.separator(); + } + 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; } - }); - 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 }); - } - } - - self.items = new_lsp_items; - self.other_servers_start_index = other_servers_start_index; - }); - } - - fn server_info(&self, ix: usize) -> Option { - 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(), - }), + 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 { + 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>) { - self.selected_index = ix; - cx.notify(); - } - - fn update_matches( - &mut self, - _: String, - _: &mut Window, - cx: &mut Context>, - ) -> 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 { - Arc::default() - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - 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::().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>) { - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - 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::().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, - _: &mut Window, - cx: &mut Context>, - ) -> Div { - div().child(div().track_focus(&editor.focus_handle(cx))) - } - - fn separators_after_indices(&self) -> Vec { - 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>, + popover_menu_handle: PopoverMenuHandle, window: &mut Window, cx: &mut Context, ) -> Self { let settings_subscription = cx.observe_global_in::(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, ) { - 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, + 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::>(); + + 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, - ) -> Entity> { - 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::()) { if Some(&editor) != self - .state + .server_state .read(cx) .active_editor .as_ref() @@ -819,25 +748,24 @@ impl StatusItemView for LspTool { window, |lsp_tool, _, e: &EditorEvent, window, cx| match e { EditorEvent::ExcerptsAdded { buffer, .. } => { - lsp_tool.state.update(cx, |state, cx| { + let updated = lsp_tool.server_state.update(cx, |state, cx| { if let Some(active_editor) = state.active_editor.as_mut() { let buffer_id = buffer.read(cx).remote_id(); - if active_editor.editor_buffers.insert(buffer_id) { - if let Some(picker) = &lsp_tool.lsp_picker { - picker.update(cx, |picker, cx| { - picker.refresh(window, cx) - }); - } - } + active_editor.editor_buffers.insert(buffer_id) + } else { + false } }); + if updated { + lsp_tool.refresh_lsp_menu(false, window, cx); + } } EditorEvent::ExcerptsRemoved { removed_buffer_ids, .. } => { - lsp_tool.state.update(cx, |state, cx| { + let removed = lsp_tool.server_state.update(cx, |state, _| { + let mut removed = false; if let Some(active_editor) = state.active_editor.as_mut() { - let mut removed = false; for id in removed_buffer_ids { active_editor.editor_buffers.retain(|buffer_id| { let retain = buffer_id != id; @@ -845,68 +773,53 @@ impl StatusItemView for LspTool { retain }); } - if removed { - if let Some(picker) = &lsp_tool.lsp_picker { - picker.update(cx, |picker, cx| { - picker.refresh(window, cx) - }); - } - } } + removed }); + if removed { + lsp_tool.refresh_lsp_menu(false, window, cx); + } } _ => {} }, ); - self.state.update(cx, |state, _| { + self.server_state.update(cx, |state, _| { state.active_editor = Some(ActiveEditor { editor: editor.downgrade(), _editor_subscription, editor_buffers, }); }); - - let lsp_picker = Self::new_lsp_picker(self.state.clone(), window, cx); - self.lsp_picker = Some(lsp_picker.clone()); - lsp_picker.update(cx, |lsp_picker, cx| lsp_picker.refresh(window, cx)); + self.refresh_lsp_menu(true, window, cx); } - } else if self.state.read(cx).active_editor.is_some() { - self.state.update(cx, |state, _| { + } else if self.server_state.read(cx).active_editor.is_some() { + self.server_state.update(cx, |state, _| { state.active_editor = None; }); - if let Some(lsp_picker) = self.lsp_picker.as_ref() { - lsp_picker.update(cx, |lsp_picker, cx| { - lsp_picker.refresh(window, cx); - }); - }; + self.refresh_lsp_menu(false, window, cx); } - } else if self.state.read(cx).active_editor.is_some() { - self.state.update(cx, |state, _| { + } else if self.server_state.read(cx).active_editor.is_some() { + self.server_state.update(cx, |state, _| { state.active_editor = None; }); - if let Some(lsp_picker) = self.lsp_picker.as_ref() { - lsp_picker.update(cx, |lsp_picker, cx| { - lsp_picker.refresh(window, cx); - }); - } + self.refresh_lsp_menu(false, window, cx); } } } impl Render for LspTool { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - if !cx.is_staff() || self.state.read(cx).language_servers.is_empty() { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + if !cx.is_staff() + || self.server_state.read(cx).language_servers.is_empty() + || self.lsp_menu.is_none() + { return div(); } - let Some(lsp_picker) = self.lsp_picker.clone() else { - return div(); - }; - let mut has_errors = false; let mut has_warnings = false; let mut has_other_notifications = false; - let state = self.state.read(cx); + 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 { .. }); @@ -933,19 +846,21 @@ impl Render for LspTool { None }; + let lsp_tool = cx.entity().clone(); div().child( - PickerPopoverMenu::new( - lsp_picker.clone(), - IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt) - .when_some(indicator, IconButton::indicator) - .icon_size(IconSize::Small) - .indicator_border_color(Some(cx.theme().colors().status_bar_background)), - move |window, cx| Tooltip::for_action("Language Servers", &ToggleMenu, window, cx), - Corner::BottomLeft, - cx, - ) - .with_handle(self.popover_menu_handle.clone()) - .render(window, cx), + PopoverMenu::new("lsp-tool") + .menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone()) + .anchor(Corner::BottomLeft) + .with_handle(self.popover_menu_handle.clone()) + .trigger_with_tooltip( + IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt) + .when_some(indicator, IconButton::indicator) + .icon_size(IconSize::Small) + .indicator_border_color(Some(cx.theme().colors().status_bar_background)), + move |window, cx| { + Tooltip::for_action("Language Servers", &ToggleMenu, window, cx) + }, + ), ) } } diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index d7080f21f4..075cf7a7d7 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -24,6 +24,7 @@ pub enum ContextMenuItem { entry_render: Box AnyElement>, handler: Rc, &mut Window, &mut App)>, selectable: bool, + documentation_aside: Option, }, } @@ -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, ) -> 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 AnyElement>, } +impl DocumentationAside { + pub fn new(side: DocumentationSide, render: Rc 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(); diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 077c18f69e..55ce0218c7 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -105,6 +105,24 @@ impl PopoverMenuHandle { .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 Option>>, + ) { + 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 {