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