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