lsp_button.rs

   1use std::{
   2    cell::RefCell,
   3    collections::{BTreeMap, HashMap},
   4    path::{Path, PathBuf},
   5    rc::Rc,
   6    time::{Duration, Instant},
   7};
   8
   9use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System};
  10
  11use client::proto;
  12use collections::HashSet;
  13use editor::{Editor, EditorEvent};
  14use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
  15use language::{BinaryStatus, BufferId, ServerHealth};
  16use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
  17use project::{
  18    LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
  19    project_settings::ProjectSettings,
  20};
  21use settings::{Settings as _, SettingsStore};
  22use ui::{
  23    ContextMenu, ContextMenuEntry, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
  24};
  25
  26use util::{ResultExt, paths::PathExt, rel_path::RelPath};
  27use workspace::{StatusItemView, Workspace};
  28
  29use crate::lsp_log_view;
  30
  31actions!(
  32    lsp_tool,
  33    [
  34        /// Toggles the language server tool menu.
  35        ToggleMenu
  36    ]
  37);
  38
  39pub struct LspButton {
  40    server_state: Entity<LanguageServerState>,
  41    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
  42    lsp_menu: Option<Entity<ContextMenu>>,
  43    lsp_menu_refresh: Task<()>,
  44    _subscriptions: Vec<Subscription>,
  45}
  46
  47struct LanguageServerState {
  48    items: Vec<LspMenuItem>,
  49    workspace: WeakEntity<Workspace>,
  50    lsp_store: WeakEntity<LspStore>,
  51    active_editor: Option<ActiveEditor>,
  52    language_servers: LanguageServers,
  53    process_memory_cache: Rc<RefCell<ProcessMemoryCache>>,
  54}
  55
  56impl std::fmt::Debug for LanguageServerState {
  57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  58        f.debug_struct("LanguageServerState")
  59            .field("items", &self.items)
  60            .field("workspace", &self.workspace)
  61            .field("lsp_store", &self.lsp_store)
  62            .field("active_editor", &self.active_editor)
  63            .field("language_servers", &self.language_servers)
  64            .finish_non_exhaustive()
  65    }
  66}
  67
  68const PROCESS_MEMORY_CACHE_DURATION: Duration = Duration::from_secs(5);
  69
  70struct ProcessMemoryCache {
  71    system: System,
  72    memory_usage: HashMap<u32, u64>,
  73    last_refresh: Option<Instant>,
  74}
  75
  76impl ProcessMemoryCache {
  77    fn new() -> Self {
  78        Self {
  79            system: System::new(),
  80            memory_usage: HashMap::new(),
  81            last_refresh: None,
  82        }
  83    }
  84
  85    fn get_memory_usage(&mut self, process_id: u32) -> u64 {
  86        let cache_expired = self
  87            .last_refresh
  88            .map(|last| last.elapsed() >= PROCESS_MEMORY_CACHE_DURATION)
  89            .unwrap_or(true);
  90
  91        if cache_expired {
  92            let refresh_kind = RefreshKind::nothing()
  93                .with_processes(ProcessRefreshKind::nothing().without_tasks().with_memory());
  94            self.system.refresh_specifics(refresh_kind);
  95            self.memory_usage.clear();
  96            self.last_refresh = Some(Instant::now());
  97        }
  98
  99        if let Some(&memory) = self.memory_usage.get(&process_id) {
 100            return memory;
 101        }
 102
 103        let root_pid = Pid::from_u32(process_id);
 104
 105        let parent_map: HashMap<Pid, Pid> = self
 106            .system
 107            .processes()
 108            .iter()
 109            .filter_map(|(&pid, process)| Some((pid, process.parent()?)))
 110            .collect();
 111
 112        let total_memory = self
 113            .system
 114            .processes()
 115            .iter()
 116            .filter(|(pid, _)| self.is_descendant_of(**pid, root_pid, &parent_map))
 117            .map(|(_, process)| process.memory())
 118            .sum();
 119
 120        self.memory_usage.insert(process_id, total_memory);
 121        total_memory
 122    }
 123
 124    fn is_descendant_of(&self, pid: Pid, root_pid: Pid, parent_map: &HashMap<Pid, Pid>) -> bool {
 125        let mut current = pid;
 126        while current != root_pid {
 127            match parent_map.get(&current) {
 128                Some(&parent) => current = parent,
 129                None => return false,
 130            }
 131        }
 132        true
 133    }
 134}
 135
 136struct ActiveEditor {
 137    editor: WeakEntity<Editor>,
 138    _editor_subscription: Subscription,
 139    editor_buffers: HashSet<BufferId>,
 140}
 141
 142impl std::fmt::Debug for ActiveEditor {
 143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 144        f.debug_struct("ActiveEditor")
 145            .field("editor", &self.editor)
 146            .field("editor_buffers", &self.editor_buffers)
 147            .finish_non_exhaustive()
 148    }
 149}
 150
 151#[derive(Debug, Default, Clone)]
 152struct LanguageServers {
 153    health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
 154    binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
 155    servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
 156}
 157
 158#[derive(Debug, Clone)]
 159struct ServersForPath {
 160    servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
 161    worktree: Option<WeakEntity<Worktree>>,
 162}
 163
 164#[derive(Debug, Clone)]
 165struct LanguageServerHealthStatus {
 166    name: LanguageServerName,
 167    health: Option<(Option<SharedString>, ServerHealth)>,
 168}
 169
 170#[derive(Debug, Clone)]
 171struct LanguageServerBinaryStatus {
 172    status: BinaryStatus,
 173    message: Option<SharedString>,
 174}
 175
 176#[derive(Debug, Clone)]
 177struct ServerInfo {
 178    name: LanguageServerName,
 179    id: LanguageServerId,
 180    health: Option<ServerHealth>,
 181    binary_status: Option<LanguageServerBinaryStatus>,
 182    message: Option<SharedString>,
 183}
 184
 185impl ServerInfo {
 186    fn server_selector(&self) -> LanguageServerSelector {
 187        LanguageServerSelector::Id(self.id)
 188    }
 189
 190    fn can_stop(&self) -> bool {
 191        self.binary_status.as_ref().is_none_or(|status| {
 192            matches!(status.status, BinaryStatus::None | BinaryStatus::Starting)
 193        })
 194    }
 195}
 196
 197impl LanguageServerHealthStatus {
 198    fn health(&self) -> Option<ServerHealth> {
 199        self.health.as_ref().map(|(_, health)| *health)
 200    }
 201
 202    fn message(&self) -> Option<SharedString> {
 203        self.health
 204            .as_ref()
 205            .and_then(|(message, _)| message.clone())
 206    }
 207}
 208
 209impl LanguageServerState {
 210    fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
 211        let lsp_logs = cx
 212            .try_global::<GlobalLogStore>()
 213            .map(|lsp_logs| lsp_logs.0.clone());
 214        let Some(lsp_logs) = lsp_logs else {
 215            return menu;
 216        };
 217
 218        let server_metadata = self
 219            .lsp_store
 220            .update(cx, |lsp_store, _| {
 221                lsp_store
 222                    .language_server_statuses()
 223                    .map(|(server_id, status)| {
 224                        (
 225                            server_id,
 226                            (
 227                                status.server_version.clone(),
 228                                status.binary.as_ref().map(|b| b.path.clone()),
 229                                status.process_id,
 230                            ),
 231                        )
 232                    })
 233                    .collect::<HashMap<_, _>>()
 234            })
 235            .unwrap_or_default();
 236
 237        let process_memory_cache = self.process_memory_cache.clone();
 238
 239        let mut first_button_encountered = false;
 240        for item in &self.items {
 241            if let LspMenuItem::ToggleServersButton { restart } = item {
 242                let label = if *restart {
 243                    "Restart All Servers"
 244                } else {
 245                    "Stop All Servers"
 246                };
 247
 248                let restart = *restart;
 249
 250                let button = ContextMenuEntry::new(label).handler({
 251                    let state = cx.entity();
 252                    move |_, cx| {
 253                        let lsp_store = state.read(cx).lsp_store.clone();
 254                        lsp_store
 255                            .update(cx, |lsp_store, cx| {
 256                                if restart {
 257                                    let Some(workspace) = state.read(cx).workspace.upgrade() else {
 258                                        return;
 259                                    };
 260                                    let project = workspace.read(cx).project().clone();
 261                                    let path_style = project.read(cx).path_style(cx);
 262                                    let buffer_store = project.read(cx).buffer_store().clone();
 263                                    let buffers = state
 264                                        .read(cx)
 265                                        .language_servers
 266                                        .servers_per_buffer_abs_path
 267                                        .iter()
 268                                        .filter_map(|(abs_path, servers)| {
 269                                            let worktree =
 270                                                servers.worktree.as_ref()?.upgrade()?.read(cx);
 271                                            let relative_path =
 272                                                abs_path.strip_prefix(&worktree.abs_path()).ok()?;
 273                                            let relative_path =
 274                                                RelPath::new(relative_path, path_style)
 275                                                    .log_err()?;
 276                                            let entry = worktree.entry_for_path(&relative_path)?;
 277                                            let project_path =
 278                                                project.read(cx).path_for_entry(entry.id, cx)?;
 279                                            buffer_store.read(cx).get_by_path(&project_path)
 280                                        })
 281                                        .collect();
 282                                    let selectors = state
 283                                        .read(cx)
 284                                        .items
 285                                        .iter()
 286                                        // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
 287                                        .flat_map(|item| match item {
 288                                            LspMenuItem::Header { .. } => None,
 289                                            LspMenuItem::ToggleServersButton { .. } => None,
 290                                            LspMenuItem::WithHealthCheck { health, .. } => Some(
 291                                                LanguageServerSelector::Name(health.name.clone()),
 292                                            ),
 293                                            LspMenuItem::WithBinaryStatus {
 294                                                server_name, ..
 295                                            } => Some(LanguageServerSelector::Name(
 296                                                server_name.clone(),
 297                                            )),
 298                                        })
 299                                        .collect();
 300                                    lsp_store.restart_language_servers_for_buffers(
 301                                        buffers, selectors, cx,
 302                                    );
 303                                } else {
 304                                    lsp_store.stop_all_language_servers(cx);
 305                                }
 306                            })
 307                            .ok();
 308                    }
 309                });
 310
 311                if !first_button_encountered {
 312                    menu = menu.separator();
 313                    first_button_encountered = true;
 314                }
 315
 316                menu = menu.item(button);
 317                continue;
 318            } else if let LspMenuItem::Header { header, separator } = item {
 319                menu = menu
 320                    .when(*separator, |menu| menu.separator())
 321                    .when_some(header.as_ref(), |menu, header| menu.header(header));
 322                continue;
 323            }
 324
 325            let Some(server_info) = item.server_info() else {
 326                continue;
 327            };
 328            let server_selector = server_info.server_selector();
 329            let is_remote = self
 330                .lsp_store
 331                .update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
 332                .unwrap_or(false);
 333            let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
 334
 335            let (status_color, status_label) = server_info
 336                .binary_status
 337                .as_ref()
 338                .and_then(|binary_status| match binary_status.status {
 339                    BinaryStatus::None => None,
 340                    BinaryStatus::CheckingForUpdate
 341                    | BinaryStatus::Downloading
 342                    | BinaryStatus::Starting => Some((Color::Modified, "Starting…")),
 343                    BinaryStatus::Stopping | BinaryStatus::Stopped => {
 344                        Some((Color::Disabled, "Stopped"))
 345                    }
 346                    BinaryStatus::Failed { .. } => Some((Color::Error, "Error")),
 347                })
 348                .or_else(|| {
 349                    Some(match server_info.health? {
 350                        ServerHealth::Ok => (Color::Success, "Running"),
 351                        ServerHealth::Warning => (Color::Warning, "Warning"),
 352                        ServerHealth::Error => (Color::Error, "Error"),
 353                    })
 354                })
 355                .unwrap_or((Color::Success, "Running"));
 356
 357            let message = server_info
 358                .message
 359                .as_ref()
 360                .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
 361                .cloned();
 362
 363            let (server_version, binary_path, process_id) = server_metadata
 364                .get(&server_info.id)
 365                .map(|(version, path, process_id)| {
 366                    (
 367                        version.clone(),
 368                        path.as_ref()
 369                            .map(|p| SharedString::from(p.compact().to_string_lossy().to_string())),
 370                        *process_id,
 371                    )
 372                })
 373                .unwrap_or((None, None, None));
 374
 375            let truncated_message = message.as_ref().and_then(|message| {
 376                message
 377                    .lines()
 378                    .filter(|line| !line.trim().is_empty())
 379                    .map(SharedString::new)
 380                    .next()
 381            });
 382
 383            let submenu_server_name = server_info.name.clone();
 384            let submenu_server_info = server_info.clone();
 385
 386            menu = menu.submenu_with_colored_icon(
 387                server_info.name.0.clone(),
 388                IconName::Circle,
 389                status_color,
 390                {
 391                    let lsp_logs = lsp_logs.clone();
 392                    let message = message.clone();
 393                    let server_selector = server_selector.clone();
 394                    let workspace = self.workspace.clone();
 395                    let lsp_store = self.lsp_store.clone();
 396                    let state = cx.entity().downgrade();
 397                    let can_stop = submenu_server_info.can_stop();
 398                    let process_memory_cache = process_memory_cache.clone();
 399
 400                    move |menu, _window, _cx| {
 401                        let mut submenu = menu;
 402
 403                        if let Some(ref message) = message {
 404                            let workspace_for_message = workspace.clone();
 405                            let message_for_handler = message.clone();
 406                            let server_name_for_message = submenu_server_name.clone();
 407                            submenu = submenu.entry("View Message", None, move |window, cx| {
 408                                let Some(create_buffer) = workspace_for_message
 409                                    .update(cx, |workspace, cx| {
 410                                        workspace.project().update(cx, |project, cx| {
 411                                            project.create_buffer(None, false, cx)
 412                                        })
 413                                    })
 414                                    .ok()
 415                                else {
 416                                    return;
 417                                };
 418
 419                                let window_handle = window.window_handle();
 420                                let workspace = workspace_for_message.clone();
 421                                let message = message_for_handler.clone();
 422                                let server_name = server_name_for_message.clone();
 423                                cx.spawn(async move |cx| {
 424                                    let buffer = create_buffer.await?;
 425                                    buffer.update(cx, |buffer, cx| {
 426                                        buffer.edit(
 427                                            [(
 428                                                0..0,
 429                                                format!(
 430                                                    "Language server {server_name}:\n\n{message}"
 431                                                ),
 432                                            )],
 433                                            None,
 434                                            cx,
 435                                        );
 436                                        buffer.set_capability(language::Capability::ReadOnly, cx);
 437                                    });
 438
 439                                    workspace.update(cx, |workspace, cx| {
 440                                        window_handle.update(cx, |_, window, cx| {
 441                                            workspace.add_item_to_active_pane(
 442                                                Box::new(cx.new(|cx| {
 443                                                    let mut editor = Editor::for_buffer(
 444                                                        buffer, None, window, cx,
 445                                                    );
 446                                                    editor.set_read_only(true);
 447                                                    editor
 448                                                })),
 449                                                None,
 450                                                true,
 451                                                window,
 452                                                cx,
 453                                            );
 454                                        })
 455                                    })??;
 456
 457                                    anyhow::Ok(())
 458                                })
 459                                .detach();
 460                            });
 461                        }
 462
 463                        if has_logs {
 464                            let lsp_logs_for_debug = lsp_logs.clone();
 465                            let workspace_for_debug = workspace.clone();
 466                            let server_selector_for_debug = server_selector.clone();
 467                            submenu = submenu.entry("View Logs", None, move |window, cx| {
 468                                lsp_log_view::open_server_trace(
 469                                    &lsp_logs_for_debug,
 470                                    workspace_for_debug.clone(),
 471                                    server_selector_for_debug.clone(),
 472                                    window,
 473                                    cx,
 474                                );
 475                            });
 476                        }
 477
 478                        let state_for_restart = state.clone();
 479                        let workspace_for_restart = workspace.clone();
 480                        let lsp_store_for_restart = lsp_store.clone();
 481                        let server_name_for_restart = submenu_server_name.clone();
 482                        submenu = submenu.entry("Restart Server", None, move |_window, cx| {
 483                            let Some(workspace) = workspace_for_restart.upgrade() else {
 484                                return;
 485                            };
 486
 487                            let project = workspace.read(cx).project().clone();
 488                            let path_style = project.read(cx).path_style(cx);
 489                            let buffer_store = project.read(cx).buffer_store().clone();
 490
 491                            let buffers = state_for_restart
 492                                .update(cx, |state, cx| {
 493                                    let server_buffers = state
 494                                        .language_servers
 495                                        .servers_per_buffer_abs_path
 496                                        .iter()
 497                                        .filter_map(|(abs_path, servers)| {
 498                                            // Check if this server is associated with this path
 499                                            let has_server = servers.servers.values().any(|name| {
 500                                                name.as_ref() == Some(&server_name_for_restart)
 501                                            });
 502
 503                                            if !has_server {
 504                                                return None;
 505                                            }
 506
 507                                            let worktree = servers.worktree.as_ref()?.upgrade()?;
 508                                            let worktree_ref = worktree.read(cx);
 509                                            let relative_path = abs_path
 510                                                .strip_prefix(&worktree_ref.abs_path())
 511                                                .ok()?;
 512                                            let relative_path =
 513                                                RelPath::new(relative_path, path_style)
 514                                                    .log_err()?;
 515                                            let entry =
 516                                                worktree_ref.entry_for_path(&relative_path)?;
 517                                            let project_path =
 518                                                project.read(cx).path_for_entry(entry.id, cx)?;
 519
 520                                            buffer_store.read(cx).get_by_path(&project_path)
 521                                        })
 522                                        .collect::<Vec<_>>();
 523
 524                                    if server_buffers.is_empty() {
 525                                        state
 526                                            .language_servers
 527                                            .servers_per_buffer_abs_path
 528                                            .iter()
 529                                            .filter_map(|(abs_path, servers)| {
 530                                                let worktree =
 531                                                    servers.worktree.as_ref()?.upgrade()?.read(cx);
 532                                                let relative_path = abs_path
 533                                                    .strip_prefix(&worktree.abs_path())
 534                                                    .ok()?;
 535                                                let relative_path =
 536                                                    RelPath::new(relative_path, path_style)
 537                                                        .log_err()?;
 538                                                let entry =
 539                                                    worktree.entry_for_path(&relative_path)?;
 540                                                let project_path = project
 541                                                    .read(cx)
 542                                                    .path_for_entry(entry.id, cx)?;
 543                                                buffer_store.read(cx).get_by_path(&project_path)
 544                                            })
 545                                            .collect()
 546                                    } else {
 547                                        server_buffers
 548                                    }
 549                                })
 550                                .unwrap_or_default();
 551
 552                            if !buffers.is_empty() {
 553                                lsp_store_for_restart
 554                                    .update(cx, |lsp_store, cx| {
 555                                        lsp_store.restart_language_servers_for_buffers(
 556                                            buffers,
 557                                            HashSet::from_iter([LanguageServerSelector::Name(
 558                                                server_name_for_restart.clone(),
 559                                            )]),
 560                                            cx,
 561                                        );
 562                                    })
 563                                    .ok();
 564                            }
 565                        });
 566
 567                        if can_stop {
 568                            let lsp_store_for_stop = lsp_store.clone();
 569                            let server_selector_for_stop = server_selector.clone();
 570
 571                            submenu = submenu.entry("Stop Server", None, move |_window, cx| {
 572                                lsp_store_for_stop
 573                                    .update(cx, |lsp_store, cx| {
 574                                        lsp_store
 575                                            .stop_language_servers_for_buffers(
 576                                                Vec::new(),
 577                                                HashSet::from_iter([
 578                                                    server_selector_for_stop.clone()
 579                                                ]),
 580                                                cx,
 581                                            )
 582                                            .detach_and_log_err(cx);
 583                                    })
 584                                    .ok();
 585                            });
 586                        }
 587
 588                        submenu = submenu.separator().custom_row({
 589                            let binary_path = binary_path.clone();
 590                            let server_version = server_version.clone();
 591                            let truncated_message = truncated_message.clone();
 592                            let process_memory_cache = process_memory_cache.clone();
 593                            move |_, _| {
 594                                let memory_usage = process_id.map(|pid| {
 595                                    process_memory_cache.borrow_mut().get_memory_usage(pid)
 596                                });
 597
 598                                let memory_label = memory_usage.map(|bytes| {
 599                                    if bytes >= 1024 * 1024 * 1024 {
 600                                        format!(
 601                                            "{:.1} GB",
 602                                            bytes as f64 / (1024.0 * 1024.0 * 1024.0)
 603                                        )
 604                                    } else {
 605                                        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
 606                                    }
 607                                });
 608
 609                                let metadata_label =
 610                                    match (&server_version, &memory_label, &truncated_message) {
 611                                        (None, None, None) => None,
 612                                        (Some(version), None, None) => {
 613                                            Some(format!("v{}", version.as_ref()))
 614                                        }
 615                                        (None, Some(memory), None) => Some(memory.clone()),
 616                                        (Some(version), Some(memory), None) => {
 617                                            Some(format!("v{}{}", version.as_ref(), memory))
 618                                        }
 619                                        (None, None, Some(message)) => Some(message.to_string()),
 620                                        (Some(version), None, Some(message)) => Some(format!(
 621                                            "v{}\n\n{}",
 622                                            version.as_ref(),
 623                                            message.as_ref()
 624                                        )),
 625                                        (None, Some(memory), Some(message)) => {
 626                                            Some(format!("{}\n\n{}", memory, message.as_ref()))
 627                                        }
 628                                        (Some(version), Some(memory), Some(message)) => {
 629                                            Some(format!(
 630                                                "v{}{}\n\n{}",
 631                                                version.as_ref(),
 632                                                memory,
 633                                                message.as_ref()
 634                                            ))
 635                                        }
 636                                    };
 637
 638                                h_flex()
 639                                    .id("metadata-container")
 640                                    .ml_neg_1()
 641                                    .gap_1()
 642                                    .max_w(rems(164.))
 643                                    .child(
 644                                        Icon::new(IconName::Circle)
 645                                            .color(status_color)
 646                                            .size(IconSize::Small),
 647                                    )
 648                                    .child(
 649                                        Label::new(status_label)
 650                                            .size(LabelSize::Small)
 651                                            .color(Color::Muted),
 652                                    )
 653                                    .when_some(metadata_label.as_ref(), |submenu, metadata| {
 654                                        submenu
 655                                            .child(
 656                                                Icon::new(IconName::Dash)
 657                                                    .color(Color::Disabled)
 658                                                    .size(IconSize::XSmall),
 659                                            )
 660                                            .child(
 661                                                Label::new(metadata)
 662                                                    .size(LabelSize::Small)
 663                                                    .color(Color::Muted)
 664                                                    .truncate(),
 665                                            )
 666                                    })
 667                                    .when_some(binary_path.clone(), |el, path| {
 668                                        el.tooltip(Tooltip::text(path))
 669                                    })
 670                                    .into_any_element()
 671                            }
 672                        });
 673
 674                        submenu
 675                    }
 676                },
 677            );
 678        }
 679        menu
 680    }
 681}
 682
 683impl LanguageServers {
 684    fn update_binary_status(
 685        &mut self,
 686        binary_status: BinaryStatus,
 687        message: Option<&str>,
 688        name: LanguageServerName,
 689    ) {
 690        let binary_status_message = message.map(SharedString::new);
 691        if matches!(
 692            binary_status,
 693            BinaryStatus::Stopped | BinaryStatus::Failed { .. }
 694        ) {
 695            self.health_statuses.retain(|_, server| server.name != name);
 696        }
 697        self.binary_statuses.insert(
 698            name,
 699            LanguageServerBinaryStatus {
 700                status: binary_status,
 701                message: binary_status_message,
 702            },
 703        );
 704    }
 705
 706    fn update_server_health(
 707        &mut self,
 708        id: LanguageServerId,
 709        health: ServerHealth,
 710        message: Option<&str>,
 711        name: Option<LanguageServerName>,
 712    ) {
 713        if let Some(state) = self.health_statuses.get_mut(&id) {
 714            state.health = Some((message.map(SharedString::new), health));
 715            if let Some(name) = name {
 716                state.name = name;
 717            }
 718        } else if let Some(name) = name {
 719            self.health_statuses.insert(
 720                id,
 721                LanguageServerHealthStatus {
 722                    health: Some((message.map(SharedString::new), health)),
 723                    name,
 724                },
 725            );
 726        }
 727    }
 728
 729    fn is_empty(&self) -> bool {
 730        self.binary_statuses.is_empty() && self.health_statuses.is_empty()
 731    }
 732}
 733
 734#[derive(Debug)]
 735enum ServerData<'a> {
 736    WithHealthCheck {
 737        server_id: LanguageServerId,
 738        health: &'a LanguageServerHealthStatus,
 739        binary_status: Option<&'a LanguageServerBinaryStatus>,
 740    },
 741    WithBinaryStatus {
 742        server_id: LanguageServerId,
 743        server_name: &'a LanguageServerName,
 744        binary_status: &'a LanguageServerBinaryStatus,
 745    },
 746}
 747
 748#[derive(Debug)]
 749enum LspMenuItem {
 750    WithHealthCheck {
 751        server_id: LanguageServerId,
 752        health: LanguageServerHealthStatus,
 753        binary_status: Option<LanguageServerBinaryStatus>,
 754    },
 755    WithBinaryStatus {
 756        server_id: LanguageServerId,
 757        server_name: LanguageServerName,
 758        binary_status: LanguageServerBinaryStatus,
 759    },
 760    ToggleServersButton {
 761        restart: bool,
 762    },
 763    Header {
 764        header: Option<SharedString>,
 765        separator: bool,
 766    },
 767}
 768
 769impl LspMenuItem {
 770    fn server_info(&self) -> Option<ServerInfo> {
 771        match self {
 772            Self::Header { .. } => None,
 773            Self::ToggleServersButton { .. } => None,
 774            Self::WithHealthCheck {
 775                server_id,
 776                health,
 777                binary_status,
 778                ..
 779            } => Some(ServerInfo {
 780                name: health.name.clone(),
 781                id: *server_id,
 782                health: health.health(),
 783                binary_status: binary_status.clone(),
 784                message: health.message(),
 785            }),
 786            Self::WithBinaryStatus {
 787                server_id,
 788                server_name,
 789                binary_status,
 790                ..
 791            } => Some(ServerInfo {
 792                name: server_name.clone(),
 793                id: *server_id,
 794                health: None,
 795                binary_status: Some(binary_status.clone()),
 796                message: binary_status.message.clone(),
 797            }),
 798        }
 799    }
 800}
 801
 802impl ServerData<'_> {
 803    fn into_lsp_item(self) -> LspMenuItem {
 804        match self {
 805            Self::WithHealthCheck {
 806                server_id,
 807                health,
 808                binary_status,
 809                ..
 810            } => LspMenuItem::WithHealthCheck {
 811                server_id,
 812                health: health.clone(),
 813                binary_status: binary_status.cloned(),
 814            },
 815            Self::WithBinaryStatus {
 816                server_id,
 817                server_name,
 818                binary_status,
 819                ..
 820            } => LspMenuItem::WithBinaryStatus {
 821                server_id,
 822                server_name: server_name.clone(),
 823                binary_status: binary_status.clone(),
 824            },
 825        }
 826    }
 827}
 828
 829impl LspButton {
 830    pub fn new(
 831        workspace: &Workspace,
 832        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
 833        window: &mut Window,
 834        cx: &mut Context<Self>,
 835    ) -> Self {
 836        let settings_subscription =
 837            cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
 838                if ProjectSettings::get_global(cx).global_lsp_settings.button {
 839                    if lsp_button.lsp_menu.is_none() {
 840                        lsp_button.refresh_lsp_menu(true, window, cx);
 841                    }
 842                } else if lsp_button.lsp_menu.take().is_some() {
 843                    cx.notify();
 844                }
 845            });
 846
 847        let lsp_store = workspace.project().read(cx).lsp_store();
 848        let mut language_servers = LanguageServers::default();
 849        for (_, status) in lsp_store.read(cx).language_server_statuses() {
 850            language_servers.binary_statuses.insert(
 851                status.name.clone(),
 852                LanguageServerBinaryStatus {
 853                    status: BinaryStatus::None,
 854                    message: None,
 855                },
 856            );
 857        }
 858
 859        let lsp_store_subscription =
 860            cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
 861                lsp_button.on_lsp_store_event(e, window, cx)
 862            });
 863
 864        let server_state = cx.new(|_| LanguageServerState {
 865            workspace: workspace.weak_handle(),
 866            items: Vec::new(),
 867            lsp_store: lsp_store.downgrade(),
 868            active_editor: None,
 869            language_servers,
 870            process_memory_cache: Rc::new(RefCell::new(ProcessMemoryCache::new())),
 871        });
 872
 873        let mut lsp_button = Self {
 874            server_state,
 875            popover_menu_handle,
 876            lsp_menu: None,
 877            lsp_menu_refresh: Task::ready(()),
 878            _subscriptions: vec![settings_subscription, lsp_store_subscription],
 879        };
 880        if !lsp_button
 881            .server_state
 882            .read(cx)
 883            .language_servers
 884            .binary_statuses
 885            .is_empty()
 886        {
 887            lsp_button.refresh_lsp_menu(true, window, cx);
 888        }
 889
 890        lsp_button
 891    }
 892
 893    fn on_lsp_store_event(
 894        &mut self,
 895        e: &LspStoreEvent,
 896        window: &mut Window,
 897        cx: &mut Context<Self>,
 898    ) {
 899        if self.lsp_menu.is_none() {
 900            return;
 901        };
 902        let mut updated = false;
 903
 904        // TODO `LspStore` is global and reports status from all language servers, even from the other windows.
 905        // Also, we do not get "LSP removed" events so LSPs are never removed.
 906        match e {
 907            LspStoreEvent::LanguageServerUpdate {
 908                language_server_id,
 909                name,
 910                message: proto::update_language_server::Variant::StatusUpdate(status_update),
 911            } => match &status_update.status {
 912                Some(proto::status_update::Status::Binary(binary_status)) => {
 913                    let Some(name) = name.as_ref() else {
 914                        return;
 915                    };
 916                    if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
 917                    {
 918                        let binary_status = match binary_status {
 919                            proto::ServerBinaryStatus::None => BinaryStatus::None,
 920                            proto::ServerBinaryStatus::CheckingForUpdate => {
 921                                BinaryStatus::CheckingForUpdate
 922                            }
 923                            proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
 924                            proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
 925                            proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
 926                            proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
 927                            proto::ServerBinaryStatus::Failed => {
 928                                let Some(error) = status_update.message.clone() else {
 929                                    return;
 930                                };
 931                                BinaryStatus::Failed { error }
 932                            }
 933                        };
 934                        self.server_state.update(cx, |state, _| {
 935                            state.language_servers.update_binary_status(
 936                                binary_status,
 937                                status_update.message.as_deref(),
 938                                name.clone(),
 939                            );
 940                        });
 941                        updated = true;
 942                    };
 943                }
 944                Some(proto::status_update::Status::Health(health_status)) => {
 945                    if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
 946                        let health = match health {
 947                            proto::ServerHealth::Ok => ServerHealth::Ok,
 948                            proto::ServerHealth::Warning => ServerHealth::Warning,
 949                            proto::ServerHealth::Error => ServerHealth::Error,
 950                        };
 951                        self.server_state.update(cx, |state, _| {
 952                            state.language_servers.update_server_health(
 953                                *language_server_id,
 954                                health,
 955                                status_update.message.as_deref(),
 956                                name.clone(),
 957                            );
 958                        });
 959                        updated = true;
 960                    }
 961                }
 962                None => {}
 963            },
 964            LspStoreEvent::LanguageServerUpdate {
 965                language_server_id,
 966                name,
 967                message: proto::update_language_server::Variant::RegisteredForBuffer(update),
 968                ..
 969            } => {
 970                self.server_state.update(cx, |state, cx| {
 971                    let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
 972                        workspace
 973                            .project()
 974                            .read(cx)
 975                            .find_worktree(Path::new(&update.buffer_abs_path), cx)
 976                            .map(|(worktree, _)| worktree.downgrade())
 977                    }) else {
 978                        return;
 979                    };
 980                    let entry = state
 981                        .language_servers
 982                        .servers_per_buffer_abs_path
 983                        .entry(PathBuf::from(&update.buffer_abs_path))
 984                        .or_insert_with(|| ServersForPath {
 985                            servers: HashMap::default(),
 986                            worktree: worktree.clone(),
 987                        });
 988                    entry.servers.insert(*language_server_id, name.clone());
 989                    if worktree.is_some() {
 990                        entry.worktree = worktree;
 991                    }
 992                });
 993                updated = true;
 994            }
 995            _ => {}
 996        };
 997
 998        if updated {
 999            self.refresh_lsp_menu(false, window, cx);
1000        }
1001    }
1002
1003    fn regenerate_items(&mut self, cx: &mut App) {
1004        self.server_state.update(cx, |state, cx| {
1005            let active_worktrees = state
1006                .active_editor
1007                .as_ref()
1008                .into_iter()
1009                .flat_map(|active_editor| {
1010                    active_editor
1011                        .editor
1012                        .upgrade()
1013                        .into_iter()
1014                        .flat_map(|active_editor| {
1015                            active_editor
1016                                .read(cx)
1017                                .buffer()
1018                                .read(cx)
1019                                .all_buffers()
1020                                .into_iter()
1021                                .filter_map(|buffer| {
1022                                    project::File::from_dyn(buffer.read(cx).file())
1023                                })
1024                                .map(|buffer_file| buffer_file.worktree.clone())
1025                        })
1026                })
1027                .collect::<HashSet<_>>();
1028
1029            let mut server_ids_to_worktrees =
1030                HashMap::<LanguageServerId, Entity<Worktree>>::default();
1031            let mut server_names_to_worktrees = HashMap::<
1032                LanguageServerName,
1033                HashSet<(Entity<Worktree>, LanguageServerId)>,
1034            >::default();
1035            for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
1036                if let Some(worktree) = servers_for_path
1037                    .worktree
1038                    .as_ref()
1039                    .and_then(|worktree| worktree.upgrade())
1040                {
1041                    for (server_id, server_name) in &servers_for_path.servers {
1042                        server_ids_to_worktrees.insert(*server_id, worktree.clone());
1043                        if let Some(server_name) = server_name {
1044                            server_names_to_worktrees
1045                                .entry(server_name.clone())
1046                                .or_default()
1047                                .insert((worktree.clone(), *server_id));
1048                        }
1049                    }
1050                }
1051            }
1052            state
1053                .lsp_store
1054                .update(cx, |lsp_store, cx| {
1055                    for (server_id, status) in lsp_store.language_server_statuses() {
1056                        if let Some(worktree) = status.worktree.and_then(|worktree_id| {
1057                            lsp_store
1058                                .worktree_store()
1059                                .read(cx)
1060                                .worktree_for_id(worktree_id, cx)
1061                        }) {
1062                            server_ids_to_worktrees.insert(server_id, worktree.clone());
1063                            server_names_to_worktrees
1064                                .entry(status.name.clone())
1065                                .or_default()
1066                                .insert((worktree, server_id));
1067                        }
1068                    }
1069                })
1070                .ok();
1071
1072            let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
1073            let mut servers_with_health_checks = HashSet::default();
1074
1075            for (server_id, health) in &state.language_servers.health_statuses {
1076                let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
1077                    let worktrees = server_names_to_worktrees.get(&health.name)?;
1078                    worktrees
1079                        .iter()
1080                        .find(|(worktree, _)| active_worktrees.contains(worktree))
1081                        .or_else(|| worktrees.iter().next())
1082                        .map(|(worktree, _)| worktree)
1083                });
1084                servers_with_health_checks.insert(&health.name);
1085                let worktree_name =
1086                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name_str()));
1087
1088                let binary_status = state.language_servers.binary_statuses.get(&health.name);
1089                let server_data = ServerData::WithHealthCheck {
1090                    server_id: *server_id,
1091                    health,
1092                    binary_status,
1093                };
1094                if let Some(worktree_name) = worktree_name {
1095                    servers_per_worktree
1096                        .entry(worktree_name.clone())
1097                        .or_default()
1098                        .push(server_data);
1099                }
1100            }
1101
1102            let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
1103            let mut can_restart_all = state.language_servers.health_statuses.is_empty();
1104            for (server_name, binary_status) in state
1105                .language_servers
1106                .binary_statuses
1107                .iter()
1108                .filter(|(name, _)| !servers_with_health_checks.contains(name))
1109            {
1110                match binary_status.status {
1111                    BinaryStatus::None => {
1112                        can_restart_all = false;
1113                        can_stop_all |= true;
1114                    }
1115                    BinaryStatus::CheckingForUpdate => {
1116                        can_restart_all = false;
1117                        can_stop_all = false;
1118                    }
1119                    BinaryStatus::Downloading => {
1120                        can_restart_all = false;
1121                        can_stop_all = false;
1122                    }
1123                    BinaryStatus::Starting => {
1124                        can_restart_all = false;
1125                        can_stop_all = false;
1126                    }
1127                    BinaryStatus::Stopping => {
1128                        can_restart_all = false;
1129                        can_stop_all = false;
1130                    }
1131                    BinaryStatus::Stopped => {}
1132                    BinaryStatus::Failed { .. } => {}
1133                }
1134
1135                if let Some(worktrees_for_name) = server_names_to_worktrees.get(server_name)
1136                    && let Some((worktree, server_id)) = worktrees_for_name
1137                        .iter()
1138                        .find(|(worktree, _)| active_worktrees.contains(worktree))
1139                        .or_else(|| worktrees_for_name.iter().next())
1140                {
1141                    let worktree_name = SharedString::new(worktree.read(cx).root_name_str());
1142                    servers_per_worktree
1143                        .entry(worktree_name.clone())
1144                        .or_default()
1145                        .push(ServerData::WithBinaryStatus {
1146                            server_name,
1147                            binary_status,
1148                            server_id: *server_id,
1149                        });
1150                }
1151            }
1152
1153            let mut new_lsp_items = Vec::with_capacity(servers_per_worktree.len() + 1);
1154            for (worktree_name, worktree_servers) in servers_per_worktree {
1155                if worktree_servers.is_empty() {
1156                    continue;
1157                }
1158                new_lsp_items.push(LspMenuItem::Header {
1159                    header: Some(worktree_name),
1160                    separator: false,
1161                });
1162                new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
1163            }
1164            if !new_lsp_items.is_empty() {
1165                if can_stop_all {
1166                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1167                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
1168                } else if can_restart_all {
1169                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1170                }
1171            }
1172
1173            state.items = new_lsp_items;
1174        });
1175    }
1176
1177    fn refresh_lsp_menu(
1178        &mut self,
1179        create_if_empty: bool,
1180        window: &mut Window,
1181        cx: &mut Context<Self>,
1182    ) {
1183        if create_if_empty || self.lsp_menu.is_some() {
1184            let state = self.server_state.clone();
1185            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
1186                cx.background_executor()
1187                    .timer(Duration::from_millis(30))
1188                    .await;
1189                lsp_button
1190                    .update_in(cx, |lsp_button, window, cx| {
1191                        lsp_button.regenerate_items(cx);
1192                        let menu = ContextMenu::build(window, cx, |menu, _, cx| {
1193                            state.update(cx, |state, cx| state.fill_menu(menu, cx))
1194                        });
1195                        lsp_button.lsp_menu = Some(menu.clone());
1196                        lsp_button.popover_menu_handle.refresh_menu(
1197                            window,
1198                            cx,
1199                            Rc::new(move |_, _| Some(menu.clone())),
1200                        );
1201                        cx.notify();
1202                    })
1203                    .ok();
1204            });
1205        }
1206    }
1207}
1208
1209impl StatusItemView for LspButton {
1210    fn set_active_pane_item(
1211        &mut self,
1212        active_pane_item: Option<&dyn workspace::ItemHandle>,
1213        window: &mut Window,
1214        cx: &mut Context<Self>,
1215    ) {
1216        if ProjectSettings::get_global(cx).global_lsp_settings.button {
1217            if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
1218                if Some(&editor)
1219                    != self
1220                        .server_state
1221                        .read(cx)
1222                        .active_editor
1223                        .as_ref()
1224                        .and_then(|active_editor| active_editor.editor.upgrade())
1225                        .as_ref()
1226                {
1227                    let editor_buffers =
1228                        HashSet::from_iter(editor.read(cx).buffer().read(cx).excerpt_buffer_ids());
1229                    let _editor_subscription = cx.subscribe_in(
1230                        &editor,
1231                        window,
1232                        |lsp_button, _, e: &EditorEvent, window, cx| match e {
1233                            EditorEvent::ExcerptsAdded { buffer, .. } => {
1234                                let updated = lsp_button.server_state.update(cx, |state, cx| {
1235                                    if let Some(active_editor) = state.active_editor.as_mut() {
1236                                        let buffer_id = buffer.read(cx).remote_id();
1237                                        active_editor.editor_buffers.insert(buffer_id)
1238                                    } else {
1239                                        false
1240                                    }
1241                                });
1242                                if updated {
1243                                    lsp_button.refresh_lsp_menu(false, window, cx);
1244                                }
1245                            }
1246                            EditorEvent::ExcerptsRemoved {
1247                                removed_buffer_ids, ..
1248                            } => {
1249                                let removed = lsp_button.server_state.update(cx, |state, _| {
1250                                    let mut removed = false;
1251                                    if let Some(active_editor) = state.active_editor.as_mut() {
1252                                        for id in removed_buffer_ids {
1253                                            active_editor.editor_buffers.retain(|buffer_id| {
1254                                                let retain = buffer_id != id;
1255                                                removed |= !retain;
1256                                                retain
1257                                            });
1258                                        }
1259                                    }
1260                                    removed
1261                                });
1262                                if removed {
1263                                    lsp_button.refresh_lsp_menu(false, window, cx);
1264                                }
1265                            }
1266                            _ => {}
1267                        },
1268                    );
1269                    self.server_state.update(cx, |state, _| {
1270                        state.active_editor = Some(ActiveEditor {
1271                            editor: editor.downgrade(),
1272                            _editor_subscription,
1273                            editor_buffers,
1274                        });
1275                    });
1276                    self.refresh_lsp_menu(true, window, cx);
1277                }
1278            } else if self.server_state.read(cx).active_editor.is_some() {
1279                self.server_state.update(cx, |state, _| {
1280                    state.active_editor = None;
1281                });
1282                self.refresh_lsp_menu(false, window, cx);
1283            }
1284        } else if self.server_state.read(cx).active_editor.is_some() {
1285            self.server_state.update(cx, |state, _| {
1286                state.active_editor = None;
1287            });
1288            self.refresh_lsp_menu(false, window, cx);
1289        }
1290    }
1291}
1292
1293impl Render for LspButton {
1294    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
1295        if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
1296            return div().hidden();
1297        }
1298
1299        let mut has_errors = false;
1300        let mut has_warnings = false;
1301        let mut has_other_notifications = false;
1302        let state = self.server_state.read(cx);
1303        for binary_status in state.language_servers.binary_statuses.values() {
1304            has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
1305            has_other_notifications |= binary_status.message.is_some();
1306        }
1307
1308        for server in state.language_servers.health_statuses.values() {
1309            if let Some((message, health)) = &server.health {
1310                has_other_notifications |= message.is_some();
1311                match health {
1312                    ServerHealth::Ok => {}
1313                    ServerHealth::Warning => has_warnings = true,
1314                    ServerHealth::Error => has_errors = true,
1315                }
1316            }
1317        }
1318
1319        let (indicator, description) = if has_errors {
1320            (
1321                Some(Indicator::dot().color(Color::Error)),
1322                "Server with errors",
1323            )
1324        } else if has_warnings {
1325            (
1326                Some(Indicator::dot().color(Color::Warning)),
1327                "Server with warnings",
1328            )
1329        } else if has_other_notifications {
1330            (
1331                Some(Indicator::dot().color(Color::Modified)),
1332                "Server with notifications",
1333            )
1334        } else {
1335            (None, "All Servers Operational")
1336        };
1337
1338        let lsp_button = cx.weak_entity();
1339
1340        div().child(
1341            PopoverMenu::new("lsp-tool")
1342                .menu(move |_, cx| {
1343                    lsp_button
1344                        .read_with(cx, |lsp_button, _| lsp_button.lsp_menu.clone())
1345                        .ok()
1346                        .flatten()
1347                })
1348                .anchor(Corner::BottomLeft)
1349                .with_handle(self.popover_menu_handle.clone())
1350                .trigger_with_tooltip(
1351                    IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
1352                        .when_some(indicator, IconButton::indicator)
1353                        .icon_size(IconSize::Small)
1354                        .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
1355                    move |_window, cx| {
1356                        Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
1357                    },
1358                ),
1359        )
1360    }
1361}