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