use std::{ collections::{BTreeMap, HashMap}, path::{Path, PathBuf}, rc::Rc, time::Duration,}; use client::proto;use collections::HashSet;use editor::{Editor, EditorEvent};use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};use language::{BinaryStatus, BufferId, ServerHealth};use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};use project::{ LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore, project_settings::ProjectSettings,};use settings::{Settings as _, SettingsStore};use ui::{ Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationEdge, DocumentationSide, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,}; use util::{ResultExt, rel_path::RelPath};use workspace::{StatusItemView, Workspace}; use crate::lsp_log_view; actions!( lsp_tool, [ /// Toggles the language server tool menu. ToggleMenu ]); pub struct LspButton { server_state: Entity<LanguageServerState>, popover_menu_handle: PopoverMenuHandle<ContextMenu>, lsp_menu: Option<Entity<ContextMenu>>, lsp_menu_refresh: Task<()>, _subscriptions: Vec<Subscription>,} #[derive(Debug)]struct LanguageServerState { items: Vec<LspMenuItem>, workspace: WeakEntity<Workspace>, lsp_store: WeakEntity<LspStore>, active_editor: Option<ActiveEditor>, language_servers: LanguageServers,} 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>, binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>, servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,} #[derive(Debug, Clone)]struct ServersForPath { servers: HashMap<LanguageServerId, Option<LanguageServerName>>, worktree: Option<WeakEntity<Worktree>>,} #[derive(Debug, Clone)]struct LanguageServerHealthStatus { name: LanguageServerName, health: Option<(Option<SharedString>, ServerHealth)>,} #[derive(Debug, Clone)]struct LanguageServerBinaryStatus { status: BinaryStatus, message: Option<SharedString>,} #[derive(Debug)]struct ServerInfo { name: LanguageServerName, id: Option<LanguageServerId>, health: Option<ServerHealth>, binary_status: Option<LanguageServerBinaryStatus>, message: Option<SharedString>,} impl ServerInfo { fn server_selector(&self) -> LanguageServerSelector { self.id .map(LanguageServerSelector::Id) .unwrap_or_else(|| LanguageServerSelector::Name(self.name.clone())) }} impl LanguageServerHealthStatus { fn health(&self) -> Option<ServerHealth> { self.health.as_ref().map(|(_, health)| *health) } fn message(&self) -> Option<SharedString> { self.health .as_ref() .and_then(|(message, _)| message.clone()) }} impl LanguageServerState { fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu { let lsp_logs = cx .try_global::<GlobalLogStore>() .map(|lsp_logs| lsp_logs.0.clone()); let Some(lsp_logs) = lsp_logs else { return menu; }; let mut first_button_encountered = false; for item in &self.items { if let LspMenuItem::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 path_style = project.read(cx).path_style(cx); let buffer_store = project.read(cx).buffer_store().clone(); let buffers = state .read(cx) .language_servers .servers_per_buffer_abs_path .iter() .filter_map(|(abs_path, servers)| { let worktree = servers.worktree.as_ref()?.upgrade()?.read(cx); let relative_path = abs_path.strip_prefix(&worktree.abs_path()).ok()?; let relative_path = RelPath::new(relative_path, path_style) .log_err()?; let entry = worktree.entry_for_path(&relative_path)?; let project_path = project.read(cx).path_for_entry(entry.id, cx)?; buffer_store.read(cx).get_by_path(&project_path) }) .collect(); 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 { LspMenuItem::Header { .. } => None, LspMenuItem::ToggleServersButton { .. } => None, LspMenuItem::WithHealthCheck { health, .. } => Some( LanguageServerSelector::Name(health.name.clone()), ), LspMenuItem::WithBinaryStatus { server_name, .. } => Some(LanguageServerSelector::Name( server_name.clone(), )), }) .collect(); lsp_store.restart_language_servers_for_buffers( buffers, selectors, cx, ); } else { lsp_store.stop_all_language_servers(cx); } }) .ok(); } }); if !first_button_encountered { menu = menu.separator(); first_button_encountered = true; } menu = menu.item(button); continue; } else if let LspMenuItem::Header { header, separator } = item { menu = menu .when(*separator, |menu| menu.separator()) .when_some(header.as_ref(), |menu, header| menu.header(header)); continue; } let Some(server_info) = item.server_info() else { continue; }; let server_selector = server_info.server_selector(); let is_remote = self .lsp_store .update(cx, |lsp_store, _| lsp_store.as_remote().is_some()) .unwrap_or(false); let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector); let status_color = server_info .binary_status .as_ref() .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 message = server_info .message .as_ref() .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); let hover_label = if message.is_some() { Some("View Message") } else if has_logs { Some("View Logs") } else { None }; let server_name = server_info.name.clone(); menu = menu.item(ContextMenuItem::custom_entry( move |_, _| { h_flex() .group("menu_item") .w_full() .gap_2() .justify_between() .child( h_flex() .gap_2() .child(Indicator::dot().color(status_color)) .child(Label::new(server_name.0.clone())), ) .when_some(hover_label, |div, hover_label| { div.child( h_flex() .visible_on_hover("menu_item") .child( Label::new(hover_label) .size(LabelSize::Small) .color(Color::Muted), ) .child( Icon::new(IconName::ChevronRight) .size(IconSize::Small) .color(Color::Muted), ), ) }) .into_any_element() }, { let lsp_logs = lsp_logs.clone(); let message = message.clone(); let server_selector = server_selector.clone(); let server_name = server_info.name.clone(); let workspace = self.workspace.clone(); move |window, cx| { if let Some(message) = &message { let Some(create_buffer) = workspace .update(cx, |workspace, cx| { workspace .project() .update(cx, |project, cx| project.create_buffer(false, cx)) }) .ok() else { return; }; let window = window.window_handle(); let workspace = workspace.clone(); let message = message.clone(); let server_name = server_name.clone(); cx.spawn(async move |cx| { let buffer = create_buffer.await?; buffer.update(cx, |buffer, cx| { buffer.edit( [( 0..0, format!("Language server {server_name}:\n\n{message}"), )], None, cx, ); buffer.set_capability(language::Capability::ReadOnly, cx); })?; workspace.update(cx, |workspace, cx| { window.update(cx, |_, window, cx| { workspace.add_item_to_active_pane( Box::new(cx.new(|cx| { let mut editor = Editor::for_buffer(buffer, None, window, cx); editor.set_read_only(true); editor })), None, true, window, cx, ); }) })??; anyhow::Ok(()) }) .detach(); } else if has_logs { lsp_log_view::open_server_trace( &lsp_logs, workspace.clone(), server_selector.clone(), window, cx, ); } else { cx.propagate(); } } }, message.map(|server_message| { DocumentationAside::new( DocumentationSide::Right, DocumentationEdge::Bottom, Rc::new(move |_| Label::new(server_message.clone()).into_any_element()), ) }), )); } menu }} impl LanguageServers { fn update_binary_status( &mut self, binary_status: BinaryStatus, message: Option<&str>, name: LanguageServerName, ) { let binary_status_message = message.map(SharedString::new); if matches!( binary_status, BinaryStatus::Stopped | BinaryStatus::Failed { .. } ) { self.health_statuses.retain(|_, server| server.name != name); } self.binary_statuses.insert( name, LanguageServerBinaryStatus { status: binary_status, message: binary_status_message, }, ); } fn update_server_health( &mut self, id: LanguageServerId, health: ServerHealth, message: Option<&str>, name: Option<LanguageServerName>, ) { if let Some(state) = self.health_statuses.get_mut(&id) { state.health = Some((message.map(SharedString::new), health)); if let Some(name) = name { state.name = name; } } else if let Some(name) = name { self.health_statuses.insert( id, LanguageServerHealthStatus { health: Some((message.map(SharedString::new), health)), name, }, ); } } fn is_empty(&self) -> bool { self.binary_statuses.is_empty() && self.health_statuses.is_empty() }} #[derive(Debug)]enum ServerData<'a> { WithHealthCheck { server_id: LanguageServerId, health: &'a LanguageServerHealthStatus, binary_status: Option<&'a LanguageServerBinaryStatus>, }, WithBinaryStatus { server_id: Option<LanguageServerId>, server_name: &'a LanguageServerName, binary_status: &'a LanguageServerBinaryStatus, },} #[derive(Debug)]enum LspMenuItem { WithHealthCheck { server_id: LanguageServerId, health: LanguageServerHealthStatus, binary_status: Option<LanguageServerBinaryStatus>, }, WithBinaryStatus { server_id: Option<LanguageServerId>, server_name: LanguageServerName, binary_status: LanguageServerBinaryStatus, }, ToggleServersButton { restart: bool, }, Header { header: Option<SharedString>, separator: bool, },} impl LspMenuItem { fn server_info(&self) -> Option<ServerInfo> { match self { Self::Header { .. } => None, Self::ToggleServersButton { .. } => None, Self::WithHealthCheck { server_id, health, binary_status, .. } => Some(ServerInfo { name: health.name.clone(), id: Some(*server_id), health: health.health(), binary_status: binary_status.clone(), message: health.message(), }), Self::WithBinaryStatus { server_id, server_name, binary_status, .. } => Some(ServerInfo { name: server_name.clone(), id: *server_id, health: None, binary_status: Some(binary_status.clone()), message: binary_status.message.clone(), }), } }} impl ServerData<'_> { fn into_lsp_item(self) -> LspMenuItem { match self { Self::WithHealthCheck { server_id, health, binary_status, ..Lines 1–500 of 1074. Load more
use std::{ collections::{BTreeMap, HashMap}, path::{Path, PathBuf}, rc::Rc, time::Duration, }; use client::proto; use collections::HashSet; use editor::{Editor, EditorEvent}; use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions}; use language::{BinaryStatus, BufferId, ServerHealth}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; use project::{ LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore, project_settings::ProjectSettings, }; use settings::{Settings as _, SettingsStore}; use ui::{ Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationEdge, DocumentationSide, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*, }; use util::{ResultExt, rel_path::RelPath}; use workspace::{StatusItemView, Workspace}; use crate::lsp_log_view; actions!( lsp_tool, [ /// Toggles the language server tool menu. ToggleMenu ] ); pub struct LspButton { server_state: Entity<LanguageServerState>, popover_menu_handle: PopoverMenuHandle<ContextMenu>, lsp_menu: Option<Entity<ContextMenu>>, lsp_menu_refresh: Task<()>, _subscriptions: Vec<Subscription>, } #[derive(Debug)] struct LanguageServerState { items: Vec<LspMenuItem>, workspace: WeakEntity<Workspace>, lsp_store: WeakEntity<LspStore>, active_editor: Option<ActiveEditor>, language_servers: LanguageServers, } 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>, binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>, servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>, } #[derive(Debug, Clone)] struct ServersForPath { servers: HashMap<LanguageServerId, Option<LanguageServerName>>, worktree: Option<WeakEntity<Worktree>>, } #[derive(Debug, Clone)] struct LanguageServerHealthStatus { name: LanguageServerName, health: Option<(Option<SharedString>, ServerHealth)>, } #[derive(Debug, Clone)] struct LanguageServerBinaryStatus { status: BinaryStatus, message: Option<SharedString>, } #[derive(Debug)] struct ServerInfo { name: LanguageServerName, id: Option<LanguageServerId>, health: Option<ServerHealth>, binary_status: Option<LanguageServerBinaryStatus>, message: Option<SharedString>, } impl ServerInfo { fn server_selector(&self) -> LanguageServerSelector { self.id .map(LanguageServerSelector::Id) .unwrap_or_else(|| LanguageServerSelector::Name(self.name.clone())) } } impl LanguageServerHealthStatus { fn health(&self) -> Option<ServerHealth> { self.health.as_ref().map(|(_, health)| *health) } fn message(&self) -> Option<SharedString> { self.health .as_ref() .and_then(|(message, _)| message.clone()) } } impl LanguageServerState { fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu { let lsp_logs = cx .try_global::<GlobalLogStore>() .map(|lsp_logs| lsp_logs.0.clone()); let Some(lsp_logs) = lsp_logs else { return menu; }; let mut first_button_encountered = false; for item in &self.items { if let LspMenuItem::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 path_style = project.read(cx).path_style(cx); let buffer_store = project.read(cx).buffer_store().clone(); let buffers = state .read(cx) .language_servers .servers_per_buffer_abs_path .iter() .filter_map(|(abs_path, servers)| { let worktree = servers.worktree.as_ref()?.upgrade()?.read(cx); let relative_path = abs_path.strip_prefix(&worktree.abs_path()).ok()?; let relative_path = RelPath::new(relative_path, path_style) .log_err()?; let entry = worktree.entry_for_path(&relative_path)?; let project_path = project.read(cx).path_for_entry(entry.id, cx)?; buffer_store.read(cx).get_by_path(&project_path) }) .collect(); 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 { LspMenuItem::Header { .. } => None, LspMenuItem::ToggleServersButton { .. } => None, LspMenuItem::WithHealthCheck { health, .. } => Some( LanguageServerSelector::Name(health.name.clone()), ), LspMenuItem::WithBinaryStatus { server_name, .. } => Some(LanguageServerSelector::Name( server_name.clone(), )), }) .collect(); lsp_store.restart_language_servers_for_buffers( buffers, selectors, cx, ); } else { lsp_store.stop_all_language_servers(cx); } }) .ok(); } }); if !first_button_encountered { menu = menu.separator(); first_button_encountered = true; } menu = menu.item(button); continue; } else if let LspMenuItem::Header { header, separator } = item { menu = menu .when(*separator, |menu| menu.separator()) .when_some(header.as_ref(), |menu, header| menu.header(header)); continue; } let Some(server_info) = item.server_info() else { continue; }; let server_selector = server_info.server_selector(); let is_remote = self .lsp_store .update(cx, |lsp_store, _| lsp_store.as_remote().is_some()) .unwrap_or(false); let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector); let status_color = server_info .binary_status .as_ref() .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 message = server_info .message .as_ref() .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); let hover_label = if message.is_some() { Some("View Message") } else if has_logs { Some("View Logs") } else { None }; let server_name = server_info.name.clone(); menu = menu.item(ContextMenuItem::custom_entry( move |_, _| { h_flex() .group("menu_item") .w_full() .gap_2() .justify_between() .child( h_flex() .gap_2() .child(Indicator::dot().color(status_color)) .child(Label::new(server_name.0.clone())), ) .when_some(hover_label, |div, hover_label| { div.child( h_flex() .visible_on_hover("menu_item") .child( Label::new(hover_label) .size(LabelSize::Small) .color(Color::Muted), ) .child( Icon::new(IconName::ChevronRight) .size(IconSize::Small) .color(Color::Muted), ), ) }) .into_any_element() }, { let lsp_logs = lsp_logs.clone(); let message = message.clone(); let server_selector = server_selector.clone(); let server_name = server_info.name.clone(); let workspace = self.workspace.clone(); move |window, cx| { if let Some(message) = &message { let Some(create_buffer) = workspace .update(cx, |workspace, cx| { workspace .project() .update(cx, |project, cx| project.create_buffer(false, cx)) }) .ok() else { return; }; let window = window.window_handle(); let workspace = workspace.clone(); let message = message.clone(); let server_name = server_name.clone(); cx.spawn(async move |cx| { let buffer = create_buffer.await?; buffer.update(cx, |buffer, cx| { buffer.edit( [( 0..0, format!("Language server {server_name}:\n\n{message}"), )], None, cx, ); buffer.set_capability(language::Capability::ReadOnly, cx); })?; workspace.update(cx, |workspace, cx| { window.update(cx, |_, window, cx| { workspace.add_item_to_active_pane( Box::new(cx.new(|cx| { let mut editor = Editor::for_buffer(buffer, None, window, cx); editor.set_read_only(true); editor })), None, true, window, cx, ); }) })??; anyhow::Ok(()) }) .detach(); } else if has_logs { lsp_log_view::open_server_trace( &lsp_logs, workspace.clone(), server_selector.clone(), window, cx, ); } else { cx.propagate(); } } }, message.map(|server_message| { DocumentationAside::new( DocumentationSide::Right, DocumentationEdge::Bottom, Rc::new(move |_| Label::new(server_message.clone()).into_any_element()), ) }), )); } menu } } impl LanguageServers { fn update_binary_status( &mut self, binary_status: BinaryStatus, message: Option<&str>, name: LanguageServerName, ) { let binary_status_message = message.map(SharedString::new); if matches!( binary_status, BinaryStatus::Stopped | BinaryStatus::Failed { .. } ) { self.health_statuses.retain(|_, server| server.name != name); } self.binary_statuses.insert( name, LanguageServerBinaryStatus { status: binary_status, message: binary_status_message, }, ); } fn update_server_health( &mut self, id: LanguageServerId, health: ServerHealth, message: Option<&str>, name: Option<LanguageServerName>, ) { if let Some(state) = self.health_statuses.get_mut(&id) { state.health = Some((message.map(SharedString::new), health)); if let Some(name) = name { state.name = name; } } else if let Some(name) = name { self.health_statuses.insert( id, LanguageServerHealthStatus { health: Some((message.map(SharedString::new), health)), name, }, ); } } fn is_empty(&self) -> bool { self.binary_statuses.is_empty() && self.health_statuses.is_empty() } } #[derive(Debug)] enum ServerData<'a> { WithHealthCheck { server_id: LanguageServerId, health: &'a LanguageServerHealthStatus, binary_status: Option<&'a LanguageServerBinaryStatus>, }, WithBinaryStatus { server_id: Option<LanguageServerId>, server_name: &'a LanguageServerName, binary_status: &'a LanguageServerBinaryStatus, }, } #[derive(Debug)] enum LspMenuItem { WithHealthCheck { server_id: LanguageServerId, health: LanguageServerHealthStatus, binary_status: Option<LanguageServerBinaryStatus>, }, WithBinaryStatus { server_id: Option<LanguageServerId>, server_name: LanguageServerName, binary_status: LanguageServerBinaryStatus, }, ToggleServersButton { restart: bool, }, Header { header: Option<SharedString>, separator: bool, }, } impl LspMenuItem { fn server_info(&self) -> Option<ServerInfo> { match self { Self::Header { .. } => None, Self::ToggleServersButton { .. } => None, Self::WithHealthCheck { server_id, health, binary_status, .. } => Some(ServerInfo { name: health.name.clone(), id: Some(*server_id), health: health.health(), binary_status: binary_status.clone(), message: health.message(), }), Self::WithBinaryStatus { server_id, server_name, binary_status, .. } => Some(ServerInfo { name: server_name.clone(), id: *server_id, health: None, binary_status: Some(binary_status.clone()), message: binary_status.message.clone(), }), } } } impl ServerData<'_> { fn into_lsp_item(self) -> LspMenuItem { match self { Self::WithHealthCheck { server_id, health, binary_status, ..