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::{ ContextMenu, ContextMenuEntry, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, 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, Clone)]struct ServerInfo { name: LanguageServerName, id: LanguageServerId, health: Option<ServerHealth>, binary_status: Option<LanguageServerBinaryStatus>, message: Option<SharedString>,} impl ServerInfo { fn server_selector(&self) -> LanguageServerSelector { LanguageServerSelector::Id(self.id) } fn can_stop(&self) -> bool { self.binary_status.as_ref().is_none_or(|status| { matches!(status.status, BinaryStatus::None | BinaryStatus::Starting) }) }} 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 server_versions = self .lsp_store .update(cx, |lsp_store, _| { lsp_store .language_server_statuses() .map(|(server_id, status)| (server_id, status.server_version.clone())) .collect::<HashMap<_, _>>() }) .unwrap_or_default(); 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, status_label) = 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, "Starting…")), BinaryStatus::Stopping | BinaryStatus::Stopped => { Some((Color::Disabled, "Stopped")) } BinaryStatus::Failed { .. } => Some((Color::Error, "Error")), }) .or_else(|| { Some(match server_info.health? { ServerHealth::Ok => (Color::Success, "Running"), ServerHealth::Warning => (Color::Warning, "Warning"), ServerHealth::Error => (Color::Error, "Error"), }) }) .unwrap_or((Color::Success, "Running")); let message = server_info .message .as_ref() .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); let server_version = server_versions .get(&server_info.id) .and_then(|version| version.clone()); let metadata_label = match (&server_version, &message) { (None, None) => None, (Some(version), None) => Some(SharedString::from(format!("v{}", version.as_ref()))), (None, Some(message)) => Some(message.clone()), (Some(version), Some(message)) => Some(SharedString::from(format!( "v{}\n\n{}", version.as_ref(), message.as_ref() ))), }; let submenu_server_name = server_info.name.clone(); let submenu_server_info = server_info.clone(); menu = menu.submenu_with_colored_icon( server_info.name.0.clone(), IconName::Circle, status_color, { let lsp_logs = lsp_logs.clone(); let message = message.clone(); let server_selector = server_selector.clone(); let workspace = self.workspace.clone(); let lsp_store = self.lsp_store.clone(); let state = cx.entity().downgrade(); let can_stop = submenu_server_info.can_stop(); move |menu, _window, _cx| { let mut submenu = menu; if let Some(ref message) = message { let workspace_for_message = workspace.clone(); let message_for_handler = message.clone(); let server_name_for_message = submenu_server_name.clone(); submenu = submenu.entry("View Message", None, move |window, cx| { let Some(create_buffer) = workspace_for_message .update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.create_buffer(false, cx) }) }) .ok() else { return; }; let window_handle = window.window_handle(); let workspace = workspace_for_message.clone(); let message = message_for_handler.clone(); let server_name = server_name_for_message.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_handle.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(); }); } if has_logs { let lsp_logs_for_debug = lsp_logs.clone(); let workspace_for_debug = workspace.clone(); let server_selector_for_debug = server_selector.clone(); submenu = submenu.entry("View Logs", None, move |window, cx| { lsp_log_view::open_server_trace( &lsp_logs_for_debug, workspace_for_debug.clone(), server_selector_for_debug.clone(), window, cx, ); }); } let state_for_restart = state.clone(); let workspace_for_restart = workspace.clone(); let lsp_store_for_restart = lsp_store.clone(); let server_name_for_restart = submenu_server_name.clone(); submenu = submenu.entry("Restart Server", None, move |_window, cx| { let Some(workspace) = workspace_for_restart.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_for_restart .update(cx, |state, cx| { let server_buffers = state .language_servers .servers_per_buffer_abs_path .iter() .filter_map(|(abs_path, servers)| { // Check if this server is associated with this path let has_server = servers.servers.values().any(|name| { name.as_ref() == Some(&server_name_for_restart) }); if !has_server { return None; } let worktree = servers.worktree.as_ref()?.upgrade()?; let worktree_ref = worktree.read(cx); let relative_path = abs_path .strip_prefix(&worktree_ref.abs_path()) .ok()?; let relative_path = RelPath::new(relative_path, path_style) .log_err()?; let entry = worktree_ref.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::<Vec<_>>(); if server_buffers.is_empty() { state .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() } else { server_buffers } }) .unwrap_or_default(); if !buffers.is_empty() { lsp_store_for_restart .update(cx, |lsp_store, cx| { lsp_store.restart_language_servers_for_buffers( buffers, HashSet::from_iter([LanguageServerSelector::Name( server_name_for_restart.clone(), )]), cx, ); }) .ok(); } }); if can_stop { let lsp_store_for_stop = lsp_store.clone(); let server_selector_for_stop = server_selector.clone(); submenu = submenu.entry("Stop Server", None, move |_window, cx| { lsp_store_for_stop .update(cx, |lsp_store, cx| { lsp_store .stop_language_servers_for_buffers( Vec::new(), HashSet::from_iter([ server_selector_for_stop.clone() ]), cx, ) .detach_and_log_err(cx); }) .ok(); }); } submenu = submenu.separator().custom_row({ let metadata_label = metadata_label.clone(); move |_, _| { h_flex() .ml_neg_1() .gap_1() .child( Icon::new(IconName::Circle) .color(status_color) .size(IconSize::Small), ) .child( Label::new(status_label)Lines 1–500 of 1207. 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::{ ContextMenu, ContextMenuEntry, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, 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, Clone)] struct ServerInfo { name: LanguageServerName, id: LanguageServerId, health: Option<ServerHealth>, binary_status: Option<LanguageServerBinaryStatus>, message: Option<SharedString>, } impl ServerInfo { fn server_selector(&self) -> LanguageServerSelector { LanguageServerSelector::Id(self.id) } fn can_stop(&self) -> bool { self.binary_status.as_ref().is_none_or(|status| { matches!(status.status, BinaryStatus::None | BinaryStatus::Starting) }) } } 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 server_versions = self .lsp_store .update(cx, |lsp_store, _| { lsp_store .language_server_statuses() .map(|(server_id, status)| (server_id, status.server_version.clone())) .collect::<HashMap<_, _>>() }) .unwrap_or_default(); 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, status_label) = 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, "Starting…")), BinaryStatus::Stopping | BinaryStatus::Stopped => { Some((Color::Disabled, "Stopped")) } BinaryStatus::Failed { .. } => Some((Color::Error, "Error")), }) .or_else(|| { Some(match server_info.health? { ServerHealth::Ok => (Color::Success, "Running"), ServerHealth::Warning => (Color::Warning, "Warning"), ServerHealth::Error => (Color::Error, "Error"), }) }) .unwrap_or((Color::Success, "Running")); let message = server_info .message .as_ref() .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) .cloned(); let server_version = server_versions .get(&server_info.id) .and_then(|version| version.clone()); let metadata_label = match (&server_version, &message) { (None, None) => None, (Some(version), None) => Some(SharedString::from(format!("v{}", version.as_ref()))), (None, Some(message)) => Some(message.clone()), (Some(version), Some(message)) => Some(SharedString::from(format!( "v{}\n\n{}", version.as_ref(), message.as_ref() ))), }; let submenu_server_name = server_info.name.clone(); let submenu_server_info = server_info.clone(); menu = menu.submenu_with_colored_icon( server_info.name.0.clone(), IconName::Circle, status_color, { let lsp_logs = lsp_logs.clone(); let message = message.clone(); let server_selector = server_selector.clone(); let workspace = self.workspace.clone(); let lsp_store = self.lsp_store.clone(); let state = cx.entity().downgrade(); let can_stop = submenu_server_info.can_stop(); move |menu, _window, _cx| { let mut submenu = menu; if let Some(ref message) = message { let workspace_for_message = workspace.clone(); let message_for_handler = message.clone(); let server_name_for_message = submenu_server_name.clone(); submenu = submenu.entry("View Message", None, move |window, cx| { let Some(create_buffer) = workspace_for_message .update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project.create_buffer(false, cx) }) }) .ok() else { return; }; let window_handle = window.window_handle(); let workspace = workspace_for_message.clone(); let message = message_for_handler.clone(); let server_name = server_name_for_message.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_handle.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(); }); } if has_logs { let lsp_logs_for_debug = lsp_logs.clone(); let workspace_for_debug = workspace.clone(); let server_selector_for_debug = server_selector.clone(); submenu = submenu.entry("View Logs", None, move |window, cx| { lsp_log_view::open_server_trace( &lsp_logs_for_debug, workspace_for_debug.clone(), server_selector_for_debug.clone(), window, cx, ); }); } let state_for_restart = state.clone(); let workspace_for_restart = workspace.clone(); let lsp_store_for_restart = lsp_store.clone(); let server_name_for_restart = submenu_server_name.clone(); submenu = submenu.entry("Restart Server", None, move |_window, cx| { let Some(workspace) = workspace_for_restart.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_for_restart .update(cx, |state, cx| { let server_buffers = state .language_servers .servers_per_buffer_abs_path .iter() .filter_map(|(abs_path, servers)| { // Check if this server is associated with this path let has_server = servers.servers.values().any(|name| { name.as_ref() == Some(&server_name_for_restart) }); if !has_server { return None; } let worktree = servers.worktree.as_ref()?.upgrade()?; let worktree_ref = worktree.read(cx); let relative_path = abs_path .strip_prefix(&worktree_ref.abs_path()) .ok()?; let relative_path = RelPath::new(relative_path, path_style) .log_err()?; let entry = worktree_ref.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::<Vec<_>>(); if server_buffers.is_empty() { state .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() } else { server_buffers } }) .unwrap_or_default(); if !buffers.is_empty() { lsp_store_for_restart .update(cx, |lsp_store, cx| { lsp_store.restart_language_servers_for_buffers( buffers, HashSet::from_iter([LanguageServerSelector::Name( server_name_for_restart.clone(), )]), cx, ); }) .ok(); } }); if can_stop { let lsp_store_for_stop = lsp_store.clone(); let server_selector_for_stop = server_selector.clone(); submenu = submenu.entry("Stop Server", None, move |_window, cx| { lsp_store_for_stop .update(cx, |lsp_store, cx| { lsp_store .stop_language_servers_for_buffers( Vec::new(), HashSet::from_iter([ server_selector_for_stop.clone() ]), cx, ) .detach_and_log_err(cx); }) .ok(); }); } submenu = submenu.separator().custom_row({ let metadata_label = metadata_label.clone(); move |_, _| { h_flex() .ml_neg_1() .gap_1() .child( Icon::new(IconName::Circle) .color(status_color) .size(IconSize::Small), ) .child( Label::new(status_label)