From 254c7a330a9c95d4ca2d7251cf601a5dac165768 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Jul 2025 20:48:07 +0300 Subject: [PATCH] Regroup LSP context menu items by the worktree name (#34838) Also * remove the feature gate * open buffers with an error when no logs are present * adjust the hover text to indicate that difference image Release Notes: - N/A --- Cargo.lock | 1 - .../src/activity_indicator.rs | 4 +- crates/language_tools/Cargo.toml | 1 - crates/language_tools/src/lsp_tool.rs | 604 +++++++++++------- 4 files changed, 363 insertions(+), 247 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1dcfb87756..4537d440cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9165,7 +9165,6 @@ dependencies = [ "collections", "copilot", "editor", - "feature_flags", "futures 0.3.31", "gpui", "itertools 0.14.0", diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index aee25fc9e3..f8ea7173d8 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -231,7 +231,6 @@ impl ActivityIndicator { status, } => { let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); - let project = project.clone(); let status = status.clone(); let server_name = server_name.clone(); cx.spawn_in(window, async move |workspace, cx| { @@ -247,8 +246,7 @@ impl ActivityIndicator { workspace.update_in(cx, |workspace, window, cx| { workspace.add_item_to_active_pane( Box::new(cx.new(|cx| { - let mut editor = - Editor::for_buffer(buffer, Some(project.clone()), window, cx); + let mut editor = Editor::for_buffer(buffer, None, window, cx); editor.set_read_only(true); editor })), diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 45af7518d5..5aa914311a 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -18,7 +18,6 @@ client.workspace = true collections.workspace = true copilot.workspace = true editor.workspace = true -feature_flags.workspace = true futures.workspace = true gpui.workspace = true itertools.workspace = true diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index fd84391680..9e95ed4673 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -1,13 +1,17 @@ -use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration}; +use std::{ + collections::{BTreeMap, HashMap}, + path::{Path, PathBuf}, + rc::Rc, + time::Duration, +}; use client::proto; -use collections::{HashMap, HashSet}; +use collections::HashSet; use editor::{Editor, EditorEvent}; -use feature_flags::FeatureFlagAppExt as _; use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions}; -use language::{BinaryStatus, BufferId, LocalFile, ServerHealth}; +use language::{BinaryStatus, BufferId, ServerHealth}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; -use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings}; +use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings}; use settings::{Settings as _, SettingsStore}; use ui::{ Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide, @@ -36,8 +40,7 @@ pub struct LspTool { #[derive(Debug)] struct LanguageServerState { - items: Vec, - other_servers_start_index: Option, + items: Vec, workspace: WeakEntity, lsp_store: WeakEntity, active_editor: Option, @@ -63,8 +66,13 @@ impl std::fmt::Debug for ActiveEditor { struct LanguageServers { health_statuses: HashMap, binary_statuses: HashMap, - servers_per_buffer_abs_path: - HashMap>>, + servers_per_buffer_abs_path: HashMap, +} + +#[derive(Debug, Clone)] +struct ServersForPath { + servers: HashMap>, + worktree: Option>, } #[derive(Debug, Clone)] @@ -120,8 +128,8 @@ impl LanguageServerState { }; let mut first_button_encountered = false; - for (i, item) in self.items.iter().enumerate() { - if let LspItem::ToggleServersButton { restart } = item { + for item in &self.items { + if let LspMenuItem::ToggleServersButton { restart } = item { let label = if *restart { "Restart All Servers" } else { @@ -140,22 +148,19 @@ impl LanguageServerState { }; 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| { + .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 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(); @@ -165,13 +170,16 @@ impl LanguageServerState { .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()), + 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( @@ -190,13 +198,17 @@ impl LanguageServerState { } 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 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 @@ -205,6 +217,7 @@ impl LanguageServerState { let status_color = server_info .binary_status + .as_ref() .and_then(|binary_status| match binary_status.status { BinaryStatus::None => None, BinaryStatus::CheckingForUpdate @@ -223,17 +236,20 @@ impl LanguageServerState { }) .unwrap_or(Color::Success); - if self - .other_servers_start_index - .is_some_and(|index| index == i) - { - menu = menu.separator().header("Other Buffers"); - } - - if i == 0 && self.other_servers_start_index.is_some() { - menu = menu.header("Current Buffer"); - } + let message = server_info + .message + .as_ref() + .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) + .cloned(); + let hover_label = if has_logs { + Some("View Logs") + } else if message.is_some() { + Some("View Message") + } else { + None + }; + let server_name = server_info.name.clone(); menu = menu.item(ContextMenuItem::custom_entry( move |_, _| { h_flex() @@ -245,42 +261,99 @@ impl LanguageServerState { h_flex() .gap_2() .child(Indicator::dot().color(status_color)) - .child(Label::new(server_info.name.0.clone())), - ) - .child( - h_flex() - .visible_on_hover("menu_item") - .child( - Label::new("View Logs") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Icon::new(IconName::ChevronRight) - .size(IconSize::Small) - .color(Color::Muted), - ), + .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 !has_logs { + if has_logs { + lsp_logs.update(cx, |lsp_logs, cx| { + lsp_logs.open_server_trace( + workspace.clone(), + server_selector.clone(), + window, + cx, + ); + }); + } else if let Some(message) = &message { + let Some(create_buffer) = workspace + .update(cx, |workspace, cx| { + workspace + .project() + .update(cx, |project, cx| project.create_buffer(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 { 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| { + message.map(|server_message| { DocumentationAside::new( DocumentationSide::Right, Rc::new(move |_| Label::new(server_message.clone()).into_any_element()), @@ -345,81 +418,95 @@ impl LanguageServers { #[derive(Debug)] enum ServerData<'a> { - WithHealthCheck( - LanguageServerId, - &'a LanguageServerHealthStatus, - Option<&'a LanguageServerBinaryStatus>, - ), - WithBinaryStatus( - Option, - &'a LanguageServerName, - &'a LanguageServerBinaryStatus, - ), -} - -#[derive(Debug)] -enum LspItem { - WithHealthCheck( - LanguageServerId, - LanguageServerHealthStatus, - Option, - ), - WithBinaryStatus( - Option, - LanguageServerName, - LanguageServerBinaryStatus, - ), - ToggleServersButton { - restart: bool, + WithHealthCheck { + server_id: LanguageServerId, + health: &'a LanguageServerHealthStatus, + binary_status: Option<&'a LanguageServerBinaryStatus>, + }, + WithBinaryStatus { + server_id: Option, + server_name: &'a LanguageServerName, + binary_status: &'a LanguageServerBinaryStatus, }, } -impl LspItem { +#[derive(Debug)] +enum LspMenuItem { + WithHealthCheck { + server_id: LanguageServerId, + health: LanguageServerHealthStatus, + binary_status: Option, + }, + WithBinaryStatus { + server_id: Option, + server_name: LanguageServerName, + binary_status: LanguageServerBinaryStatus, + }, + ToggleServersButton { + restart: bool, + }, + Header { + header: Option, + separator: bool, + }, +} + +impl LspMenuItem { 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( + Self::Header { .. } => None, + Self::ToggleServersButton { .. } => None, + Self::WithHealthCheck { server_id, - language_server_name, - language_server_binary_status, - ) => Some(ServerInfo { - name: language_server_name.clone(), + 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(language_server_binary_status.clone()), - message: language_server_binary_status.message.clone(), + binary_status: Some(binary_status.clone()), + message: binary_status.message.clone(), }), } } } impl ServerData<'_> { - fn name(&self) -> &LanguageServerName { + fn into_lsp_item(self) -> LspMenuItem { match self { - Self::WithHealthCheck(_, state, _) => &state.name, - Self::WithBinaryStatus(_, name, ..) => name, - } - } - - fn into_lsp_item(self) -> LspItem { - match self { - Self::WithHealthCheck(id, name, status) => { - LspItem::WithHealthCheck(id, name.clone(), status.cloned()) - } - Self::WithBinaryStatus(server_id, name, status) => { - LspItem::WithBinaryStatus(server_id, name.clone(), status.clone()) - } + Self::WithHealthCheck { + server_id, + health, + binary_status, + .. + } => LspMenuItem::WithHealthCheck { + server_id, + health: health.clone(), + binary_status: binary_status.cloned(), + }, + Self::WithBinaryStatus { + server_id, + server_name, + binary_status, + .. + } => LspMenuItem::WithBinaryStatus { + server_id, + server_name: server_name.clone(), + binary_status: binary_status.clone(), + }, } } } @@ -452,7 +539,6 @@ impl LspTool { 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(), @@ -542,13 +628,28 @@ impl LspTool { message: proto::update_language_server::Variant::RegisteredForBuffer(update), .. } => { - self.server_state.update(cx, |state, _| { - state + self.server_state.update(cx, |state, cx| { + let Ok(worktree) = state.workspace.update(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .find_worktree(Path::new(&update.buffer_abs_path), cx) + .map(|(worktree, _)| worktree.downgrade()) + }) else { + return; + }; + let entry = state .language_servers .servers_per_buffer_abs_path .entry(PathBuf::from(&update.buffer_abs_path)) - .or_default() - .insert(*language_server_id, name.clone()); + .or_insert_with(|| ServersForPath { + servers: HashMap::default(), + worktree: worktree.clone(), + }); + entry.servers.insert(*language_server_id, name.clone()); + if worktree.is_some() { + entry.worktree = worktree; + } }); updated = true; } @@ -562,94 +663,95 @@ impl LspTool { fn regenerate_items(&mut self, cx: &mut App) { self.server_state.update(cx, |state, cx| { - let editor_buffers = state + let active_worktrees = 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), - ) + .into_iter() + .flat_map(|active_editor| { + active_editor + .editor + .upgrade() + .into_iter() + .flat_map(|active_editor| { + active_editor + .read(cx) + .buffer() + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + project::File::from_dyn(buffer.read(cx).file()) + }) + .map(|buffer_file| buffer_file.worktree.clone()) }) - .ok()??; - Some(buffer_path) }) - .collect::>(); + .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()); + let mut server_ids_to_worktrees = + HashMap::>::default(); + let mut server_names_to_worktrees = HashMap::< + LanguageServerName, + HashSet<(Entity, LanguageServerId)>, + >::default(); + for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() { + if let Some(worktree) = servers_for_path + .worktree + .as_ref() + .and_then(|worktree| worktree.upgrade()) + { + for (server_id, server_name) in &servers_for_path.servers { + server_ids_to_worktrees.insert(*server_id, worktree.clone()); + if let Some(server_name) = server_name { + server_names_to_worktrees + .entry(server_name.clone()) + .or_default() + .insert((worktree.clone(), *server_id)); } } - acc + } + } + + let mut servers_per_worktree = BTreeMap::>::new(); + let mut servers_without_worktree = Vec::::new(); + let mut servers_with_health_checks = HashSet::default(); + + for (server_id, health) in &state.language_servers.health_statuses { + let worktree = server_ids_to_worktrees.get(server_id).or_else(|| { + let worktrees = server_names_to_worktrees.get(&health.name)?; + worktrees + .iter() + .find(|(worktree, _)| active_worktrees.contains(worktree)) + .or_else(|| worktrees.iter().next()) + .map(|(worktree, _)| worktree) }); - 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, - )); + servers_with_health_checks.insert(&health.name); + let worktree_name = + worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name())); + + let binary_status = state.language_servers.binary_statuses.get(&health.name); + let server_data = ServerData::WithHealthCheck { + server_id: *server_id, + health, + binary_status, + }; + match worktree_name { + Some(worktree_name) => servers_per_worktree + .entry(worktree_name.clone()) + .or_default() + .push(server_data), + None => servers_without_worktree.push(server_data), } } 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 + for (server_name, binary_status) in state .language_servers .binary_statuses .iter() .filter(|(name, _)| !servers_with_health_checks.contains(name)) { - match status.status { + match binary_status.status { BinaryStatus::None => { can_restart_all = false; can_stop_all |= true; @@ -674,52 +776,73 @@ impl LspTool { 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 + match server_names_to_worktrees.get(server_name) { + Some(worktrees_for_name) => { + match worktrees_for_name + .iter() + .find(|(worktree, _)| active_worktrees.contains(worktree)) + .or_else(|| worktrees_for_name.iter().next()) + { + Some((worktree, server_id)) => { + let worktree_name = + SharedString::new(worktree.read(cx).root_name()); + servers_per_worktree + .entry(worktree_name.clone()) + .or_default() + .push(ServerData::WithBinaryStatus { + server_name, + binary_status, + server_id: Some(*server_id), + }); + } + None => servers_without_worktree.push(ServerData::WithBinaryStatus { + server_name, + binary_status, + server_id: None, + }), } - }); - if let Some(server_id) = matching_server_id { - buffer_servers.push(ServerData::WithBinaryStatus( - Some(server_id), + } + None => servers_without_worktree.push(ServerData::WithBinaryStatus { server_name, - status, - )); - } else { - other_servers.push(ServerData::WithBinaryStatus(None, server_name, status)); + binary_status, + server_id: None, + }), } } - 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()); + Vec::with_capacity(servers_per_worktree.len() + servers_without_worktree.len() + 2); + for (worktree_name, worktree_servers) in servers_per_worktree { + if worktree_servers.is_empty() { + continue; + } + new_lsp_items.push(LspMenuItem::Header { + header: Some(worktree_name), + separator: false, + }); + new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item)); + } + if !servers_without_worktree.is_empty() { + new_lsp_items.push(LspMenuItem::Header { + header: Some(SharedString::from("Unknown worktree")), + separator: false, + }); + new_lsp_items.extend( + servers_without_worktree + .into_iter() + .map(ServerData::into_lsp_item), + ); } - 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: true }); - new_lsp_items.push(LspItem::ToggleServersButton { restart: false }); + new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true }); + new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false }); } else if can_restart_all { - new_lsp_items.push(LspItem::ToggleServersButton { restart: true }); + new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true }); } } state.items = new_lsp_items; - state.other_servers_start_index = other_servers_start_index; }); } @@ -841,10 +964,7 @@ impl StatusItemView for LspTool { impl Render for LspTool { 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() - { + if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() { return div(); } @@ -852,12 +972,12 @@ impl Render for LspTool { let mut has_warnings = false; let mut has_other_notifications = false; 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 { .. }); - has_other_notifications |= binary_status.message.is_some(); - } + for binary_status in state.language_servers.binary_statuses.values() { + has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. }); + has_other_notifications |= binary_status.message.is_some(); + } + for server in state.language_servers.health_statuses.values() { if let Some((message, health)) = &server.health { has_other_notifications |= message.is_some(); match health {