lsp_button.rs

   1use std::{
   2    collections::{BTreeMap, HashMap},
   3    path::{Path, PathBuf},
   4    rc::Rc,
   5    time::Duration,
   6};
   7
   8use client::proto;
   9use collections::HashSet;
  10use editor::{Editor, EditorEvent};
  11use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
  12use language::{BinaryStatus, BufferId, ServerHealth};
  13use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
  14use project::{
  15    LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
  16    project_settings::ProjectSettings,
  17};
  18use settings::{Settings as _, SettingsStore};
  19use ui::{
  20    Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationEdge,
  21    DocumentationSide, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
  22};
  23
  24use util::{ResultExt, rel_path::RelPath};
  25use workspace::{StatusItemView, Workspace};
  26
  27use crate::lsp_log_view;
  28
  29actions!(
  30    lsp_tool,
  31    [
  32        /// Toggles the language server tool menu.
  33        ToggleMenu
  34    ]
  35);
  36
  37pub struct LspButton {
  38    server_state: Entity<LanguageServerState>,
  39    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
  40    lsp_menu: Option<Entity<ContextMenu>>,
  41    lsp_menu_refresh: Task<()>,
  42    _subscriptions: Vec<Subscription>,
  43}
  44
  45#[derive(Debug)]
  46struct LanguageServerState {
  47    items: Vec<LspMenuItem>,
  48    workspace: WeakEntity<Workspace>,
  49    lsp_store: WeakEntity<LspStore>,
  50    active_editor: Option<ActiveEditor>,
  51    language_servers: LanguageServers,
  52}
  53
  54struct ActiveEditor {
  55    editor: WeakEntity<Editor>,
  56    _editor_subscription: Subscription,
  57    editor_buffers: HashSet<BufferId>,
  58}
  59
  60impl std::fmt::Debug for ActiveEditor {
  61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  62        f.debug_struct("ActiveEditor")
  63            .field("editor", &self.editor)
  64            .field("editor_buffers", &self.editor_buffers)
  65            .finish_non_exhaustive()
  66    }
  67}
  68
  69#[derive(Debug, Default, Clone)]
  70struct LanguageServers {
  71    health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
  72    binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
  73    servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
  74}
  75
  76#[derive(Debug, Clone)]
  77struct ServersForPath {
  78    servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
  79    worktree: Option<WeakEntity<Worktree>>,
  80}
  81
  82#[derive(Debug, Clone)]
  83struct LanguageServerHealthStatus {
  84    name: LanguageServerName,
  85    health: Option<(Option<SharedString>, ServerHealth)>,
  86}
  87
  88#[derive(Debug, Clone)]
  89struct LanguageServerBinaryStatus {
  90    status: BinaryStatus,
  91    message: Option<SharedString>,
  92}
  93
  94#[derive(Debug)]
  95struct ServerInfo {
  96    name: LanguageServerName,
  97    id: LanguageServerId,
  98    health: Option<ServerHealth>,
  99    binary_status: Option<LanguageServerBinaryStatus>,
 100    message: Option<SharedString>,
 101}
 102
 103impl ServerInfo {
 104    fn server_selector(&self) -> LanguageServerSelector {
 105        LanguageServerSelector::Id(self.id)
 106    }
 107}
 108
 109impl LanguageServerHealthStatus {
 110    fn health(&self) -> Option<ServerHealth> {
 111        self.health.as_ref().map(|(_, health)| *health)
 112    }
 113
 114    fn message(&self) -> Option<SharedString> {
 115        self.health
 116            .as_ref()
 117            .and_then(|(message, _)| message.clone())
 118    }
 119}
 120
 121impl LanguageServerState {
 122    fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
 123        let lsp_logs = cx
 124            .try_global::<GlobalLogStore>()
 125            .map(|lsp_logs| lsp_logs.0.clone());
 126        let Some(lsp_logs) = lsp_logs else {
 127            return menu;
 128        };
 129
 130        let mut first_button_encountered = false;
 131        for item in &self.items {
 132            if let LspMenuItem::ToggleServersButton { restart } = item {
 133                let label = if *restart {
 134                    "Restart All Servers"
 135                } else {
 136                    "Stop All Servers"
 137                };
 138                let restart = *restart;
 139                let button = ContextMenuEntry::new(label).handler({
 140                    let state = cx.entity();
 141                    move |_, cx| {
 142                        let lsp_store = state.read(cx).lsp_store.clone();
 143                        lsp_store
 144                            .update(cx, |lsp_store, cx| {
 145                                if restart {
 146                                    let Some(workspace) = state.read(cx).workspace.upgrade() else {
 147                                        return;
 148                                    };
 149                                    let project = workspace.read(cx).project().clone();
 150                                    let path_style = project.read(cx).path_style(cx);
 151                                    let buffer_store = project.read(cx).buffer_store().clone();
 152                                    let buffers = state
 153                                        .read(cx)
 154                                        .language_servers
 155                                        .servers_per_buffer_abs_path
 156                                        .iter()
 157                                        .filter_map(|(abs_path, servers)| {
 158                                            let worktree =
 159                                                servers.worktree.as_ref()?.upgrade()?.read(cx);
 160                                            let relative_path =
 161                                                abs_path.strip_prefix(&worktree.abs_path()).ok()?;
 162                                            let relative_path =
 163                                                RelPath::new(relative_path, path_style)
 164                                                    .log_err()?;
 165                                            let entry = worktree.entry_for_path(&relative_path)?;
 166                                            let project_path =
 167                                                project.read(cx).path_for_entry(entry.id, cx)?;
 168                                            buffer_store.read(cx).get_by_path(&project_path)
 169                                        })
 170                                        .collect();
 171                                    let selectors = state
 172                                        .read(cx)
 173                                        .items
 174                                        .iter()
 175                                        // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
 176                                        .flat_map(|item| match item {
 177                                            LspMenuItem::Header { .. } => None,
 178                                            LspMenuItem::ToggleServersButton { .. } => None,
 179                                            LspMenuItem::WithHealthCheck { health, .. } => Some(
 180                                                LanguageServerSelector::Name(health.name.clone()),
 181                                            ),
 182                                            LspMenuItem::WithBinaryStatus {
 183                                                server_name, ..
 184                                            } => Some(LanguageServerSelector::Name(
 185                                                server_name.clone(),
 186                                            )),
 187                                        })
 188                                        .collect();
 189                                    lsp_store.restart_language_servers_for_buffers(
 190                                        buffers, selectors, cx,
 191                                    );
 192                                } else {
 193                                    lsp_store.stop_all_language_servers(cx);
 194                                }
 195                            })
 196                            .ok();
 197                    }
 198                });
 199                if !first_button_encountered {
 200                    menu = menu.separator();
 201                    first_button_encountered = true;
 202                }
 203                menu = menu.item(button);
 204                continue;
 205            } else if let LspMenuItem::Header { header, separator } = item {
 206                menu = menu
 207                    .when(*separator, |menu| menu.separator())
 208                    .when_some(header.as_ref(), |menu, header| menu.header(header));
 209                continue;
 210            }
 211
 212            let Some(server_info) = item.server_info() else {
 213                continue;
 214            };
 215            let server_selector = server_info.server_selector();
 216            let is_remote = self
 217                .lsp_store
 218                .update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
 219                .unwrap_or(false);
 220            let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
 221
 222            let status_color = server_info
 223                .binary_status
 224                .as_ref()
 225                .and_then(|binary_status| match binary_status.status {
 226                    BinaryStatus::None => None,
 227                    BinaryStatus::CheckingForUpdate
 228                    | BinaryStatus::Downloading
 229                    | BinaryStatus::Starting => Some(Color::Modified),
 230                    BinaryStatus::Stopping => Some(Color::Disabled),
 231                    BinaryStatus::Stopped => Some(Color::Disabled),
 232                    BinaryStatus::Failed { .. } => Some(Color::Error),
 233                })
 234                .or_else(|| {
 235                    Some(match server_info.health? {
 236                        ServerHealth::Ok => Color::Success,
 237                        ServerHealth::Warning => Color::Warning,
 238                        ServerHealth::Error => Color::Error,
 239                    })
 240                })
 241                .unwrap_or(Color::Success);
 242
 243            let message = server_info
 244                .message
 245                .as_ref()
 246                .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
 247                .cloned();
 248            let hover_label = if message.is_some() {
 249                Some("View Message")
 250            } else if has_logs {
 251                Some("View Logs")
 252            } else {
 253                None
 254            };
 255
 256            let server_name = server_info.name.clone();
 257            menu = menu.item(ContextMenuItem::custom_entry(
 258                move |_, _| {
 259                    h_flex()
 260                        .group("menu_item")
 261                        .w_full()
 262                        .gap_2()
 263                        .justify_between()
 264                        .child(
 265                            h_flex()
 266                                .gap_2()
 267                                .child(Indicator::dot().color(status_color))
 268                                .child(Label::new(server_name.0.clone())),
 269                        )
 270                        .when_some(hover_label, |div, hover_label| {
 271                            div.child(
 272                                h_flex()
 273                                    .visible_on_hover("menu_item")
 274                                    .child(
 275                                        Label::new(hover_label)
 276                                            .size(LabelSize::Small)
 277                                            .color(Color::Muted),
 278                                    )
 279                                    .child(
 280                                        Icon::new(IconName::ChevronRight)
 281                                            .size(IconSize::Small)
 282                                            .color(Color::Muted),
 283                                    ),
 284                            )
 285                        })
 286                        .into_any_element()
 287                },
 288                {
 289                    let lsp_logs = lsp_logs.clone();
 290                    let message = message.clone();
 291                    let server_selector = server_selector.clone();
 292                    let server_name = server_info.name.clone();
 293                    let workspace = self.workspace.clone();
 294                    move |window, cx| {
 295                        if let Some(message) = &message {
 296                            let Some(create_buffer) = workspace
 297                                .update(cx, |workspace, cx| {
 298                                    workspace
 299                                        .project()
 300                                        .update(cx, |project, cx| project.create_buffer(false, cx))
 301                                })
 302                                .ok()
 303                            else {
 304                                return;
 305                            };
 306
 307                            let window = window.window_handle();
 308                            let workspace = workspace.clone();
 309                            let message = message.clone();
 310                            let server_name = server_name.clone();
 311                            cx.spawn(async move |cx| {
 312                                let buffer = create_buffer.await?;
 313                                buffer.update(cx, |buffer, cx| {
 314                                    buffer.edit(
 315                                        [(
 316                                            0..0,
 317                                            format!("Language server {server_name}:\n\n{message}"),
 318                                        )],
 319                                        None,
 320                                        cx,
 321                                    );
 322                                    buffer.set_capability(language::Capability::ReadOnly, cx);
 323                                })?;
 324
 325                                workspace.update(cx, |workspace, cx| {
 326                                    window.update(cx, |_, window, cx| {
 327                                        workspace.add_item_to_active_pane(
 328                                            Box::new(cx.new(|cx| {
 329                                                let mut editor =
 330                                                    Editor::for_buffer(buffer, None, window, cx);
 331                                                editor.set_read_only(true);
 332                                                editor
 333                                            })),
 334                                            None,
 335                                            true,
 336                                            window,
 337                                            cx,
 338                                        );
 339                                    })
 340                                })??;
 341
 342                                anyhow::Ok(())
 343                            })
 344                            .detach();
 345                        } else if has_logs {
 346                            lsp_log_view::open_server_trace(
 347                                &lsp_logs,
 348                                workspace.clone(),
 349                                server_selector.clone(),
 350                                window,
 351                                cx,
 352                            );
 353                        } else {
 354                            cx.propagate();
 355                        }
 356                    }
 357                },
 358                message.map(|server_message| {
 359                    DocumentationAside::new(
 360                        DocumentationSide::Right,
 361                        DocumentationEdge::Bottom,
 362                        Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
 363                    )
 364                }),
 365            ));
 366        }
 367        menu
 368    }
 369}
 370
 371impl LanguageServers {
 372    fn update_binary_status(
 373        &mut self,
 374        binary_status: BinaryStatus,
 375        message: Option<&str>,
 376        name: LanguageServerName,
 377    ) {
 378        let binary_status_message = message.map(SharedString::new);
 379        if matches!(
 380            binary_status,
 381            BinaryStatus::Stopped | BinaryStatus::Failed { .. }
 382        ) {
 383            self.health_statuses.retain(|_, server| server.name != name);
 384        }
 385        self.binary_statuses.insert(
 386            name,
 387            LanguageServerBinaryStatus {
 388                status: binary_status,
 389                message: binary_status_message,
 390            },
 391        );
 392    }
 393
 394    fn update_server_health(
 395        &mut self,
 396        id: LanguageServerId,
 397        health: ServerHealth,
 398        message: Option<&str>,
 399        name: Option<LanguageServerName>,
 400    ) {
 401        if let Some(state) = self.health_statuses.get_mut(&id) {
 402            state.health = Some((message.map(SharedString::new), health));
 403            if let Some(name) = name {
 404                state.name = name;
 405            }
 406        } else if let Some(name) = name {
 407            self.health_statuses.insert(
 408                id,
 409                LanguageServerHealthStatus {
 410                    health: Some((message.map(SharedString::new), health)),
 411                    name,
 412                },
 413            );
 414        }
 415    }
 416
 417    fn is_empty(&self) -> bool {
 418        self.binary_statuses.is_empty() && self.health_statuses.is_empty()
 419    }
 420}
 421
 422#[derive(Debug)]
 423enum ServerData<'a> {
 424    WithHealthCheck {
 425        server_id: LanguageServerId,
 426        health: &'a LanguageServerHealthStatus,
 427        binary_status: Option<&'a LanguageServerBinaryStatus>,
 428    },
 429    WithBinaryStatus {
 430        server_id: LanguageServerId,
 431        server_name: &'a LanguageServerName,
 432        binary_status: &'a LanguageServerBinaryStatus,
 433    },
 434}
 435
 436#[derive(Debug)]
 437enum LspMenuItem {
 438    WithHealthCheck {
 439        server_id: LanguageServerId,
 440        health: LanguageServerHealthStatus,
 441        binary_status: Option<LanguageServerBinaryStatus>,
 442    },
 443    WithBinaryStatus {
 444        server_id: LanguageServerId,
 445        server_name: LanguageServerName,
 446        binary_status: LanguageServerBinaryStatus,
 447    },
 448    ToggleServersButton {
 449        restart: bool,
 450    },
 451    Header {
 452        header: Option<SharedString>,
 453        separator: bool,
 454    },
 455}
 456
 457impl LspMenuItem {
 458    fn server_info(&self) -> Option<ServerInfo> {
 459        match self {
 460            Self::Header { .. } => None,
 461            Self::ToggleServersButton { .. } => None,
 462            Self::WithHealthCheck {
 463                server_id,
 464                health,
 465                binary_status,
 466                ..
 467            } => Some(ServerInfo {
 468                name: health.name.clone(),
 469                id: *server_id,
 470                health: health.health(),
 471                binary_status: binary_status.clone(),
 472                message: health.message(),
 473            }),
 474            Self::WithBinaryStatus {
 475                server_id,
 476                server_name,
 477                binary_status,
 478                ..
 479            } => Some(ServerInfo {
 480                name: server_name.clone(),
 481                id: *server_id,
 482                health: None,
 483                binary_status: Some(binary_status.clone()),
 484                message: binary_status.message.clone(),
 485            }),
 486        }
 487    }
 488}
 489
 490impl ServerData<'_> {
 491    fn into_lsp_item(self) -> LspMenuItem {
 492        match self {
 493            Self::WithHealthCheck {
 494                server_id,
 495                health,
 496                binary_status,
 497                ..
 498            } => LspMenuItem::WithHealthCheck {
 499                server_id,
 500                health: health.clone(),
 501                binary_status: binary_status.cloned(),
 502            },
 503            Self::WithBinaryStatus {
 504                server_id,
 505                server_name,
 506                binary_status,
 507                ..
 508            } => LspMenuItem::WithBinaryStatus {
 509                server_id,
 510                server_name: server_name.clone(),
 511                binary_status: binary_status.clone(),
 512            },
 513        }
 514    }
 515}
 516
 517impl LspButton {
 518    pub fn new(
 519        workspace: &Workspace,
 520        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
 521        window: &mut Window,
 522        cx: &mut Context<Self>,
 523    ) -> Self {
 524        let settings_subscription =
 525            cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
 526                if ProjectSettings::get_global(cx).global_lsp_settings.button {
 527                    if lsp_button.lsp_menu.is_none() {
 528                        lsp_button.refresh_lsp_menu(true, window, cx);
 529                    }
 530                } else if lsp_button.lsp_menu.take().is_some() {
 531                    cx.notify();
 532                }
 533            });
 534
 535        let lsp_store = workspace.project().read(cx).lsp_store();
 536        let mut language_servers = LanguageServers::default();
 537        for (_, status) in lsp_store.read(cx).language_server_statuses() {
 538            language_servers.binary_statuses.insert(
 539                status.name.clone(),
 540                LanguageServerBinaryStatus {
 541                    status: BinaryStatus::None,
 542                    message: None,
 543                },
 544            );
 545        }
 546
 547        let lsp_store_subscription =
 548            cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
 549                lsp_button.on_lsp_store_event(e, window, cx)
 550            });
 551
 552        let server_state = cx.new(|_| LanguageServerState {
 553            workspace: workspace.weak_handle(),
 554            items: Vec::new(),
 555            lsp_store: lsp_store.downgrade(),
 556            active_editor: None,
 557            language_servers,
 558        });
 559
 560        let mut lsp_button = Self {
 561            server_state,
 562            popover_menu_handle,
 563            lsp_menu: None,
 564            lsp_menu_refresh: Task::ready(()),
 565            _subscriptions: vec![settings_subscription, lsp_store_subscription],
 566        };
 567        if !lsp_button
 568            .server_state
 569            .read(cx)
 570            .language_servers
 571            .binary_statuses
 572            .is_empty()
 573        {
 574            lsp_button.refresh_lsp_menu(true, window, cx);
 575        }
 576
 577        lsp_button
 578    }
 579
 580    fn on_lsp_store_event(
 581        &mut self,
 582        e: &LspStoreEvent,
 583        window: &mut Window,
 584        cx: &mut Context<Self>,
 585    ) {
 586        if self.lsp_menu.is_none() {
 587            return;
 588        };
 589        let mut updated = false;
 590
 591        // TODO `LspStore` is global and reports status from all language servers, even from the other windows.
 592        // Also, we do not get "LSP removed" events so LSPs are never removed.
 593        match e {
 594            LspStoreEvent::LanguageServerUpdate {
 595                language_server_id,
 596                name,
 597                message: proto::update_language_server::Variant::StatusUpdate(status_update),
 598            } => match &status_update.status {
 599                Some(proto::status_update::Status::Binary(binary_status)) => {
 600                    let Some(name) = name.as_ref() else {
 601                        return;
 602                    };
 603                    if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
 604                    {
 605                        let binary_status = match binary_status {
 606                            proto::ServerBinaryStatus::None => BinaryStatus::None,
 607                            proto::ServerBinaryStatus::CheckingForUpdate => {
 608                                BinaryStatus::CheckingForUpdate
 609                            }
 610                            proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
 611                            proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
 612                            proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
 613                            proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
 614                            proto::ServerBinaryStatus::Failed => {
 615                                let Some(error) = status_update.message.clone() else {
 616                                    return;
 617                                };
 618                                BinaryStatus::Failed { error }
 619                            }
 620                        };
 621                        self.server_state.update(cx, |state, _| {
 622                            state.language_servers.update_binary_status(
 623                                binary_status,
 624                                status_update.message.as_deref(),
 625                                name.clone(),
 626                            );
 627                        });
 628                        updated = true;
 629                    };
 630                }
 631                Some(proto::status_update::Status::Health(health_status)) => {
 632                    if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
 633                        let health = match health {
 634                            proto::ServerHealth::Ok => ServerHealth::Ok,
 635                            proto::ServerHealth::Warning => ServerHealth::Warning,
 636                            proto::ServerHealth::Error => ServerHealth::Error,
 637                        };
 638                        self.server_state.update(cx, |state, _| {
 639                            state.language_servers.update_server_health(
 640                                *language_server_id,
 641                                health,
 642                                status_update.message.as_deref(),
 643                                name.clone(),
 644                            );
 645                        });
 646                        updated = true;
 647                    }
 648                }
 649                None => {}
 650            },
 651            LspStoreEvent::LanguageServerUpdate {
 652                language_server_id,
 653                name,
 654                message: proto::update_language_server::Variant::RegisteredForBuffer(update),
 655                ..
 656            } => {
 657                self.server_state.update(cx, |state, cx| {
 658                    let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
 659                        workspace
 660                            .project()
 661                            .read(cx)
 662                            .find_worktree(Path::new(&update.buffer_abs_path), cx)
 663                            .map(|(worktree, _)| worktree.downgrade())
 664                    }) else {
 665                        return;
 666                    };
 667                    let entry = state
 668                        .language_servers
 669                        .servers_per_buffer_abs_path
 670                        .entry(PathBuf::from(&update.buffer_abs_path))
 671                        .or_insert_with(|| ServersForPath {
 672                            servers: HashMap::default(),
 673                            worktree: worktree.clone(),
 674                        });
 675                    entry.servers.insert(*language_server_id, name.clone());
 676                    if worktree.is_some() {
 677                        entry.worktree = worktree;
 678                    }
 679                });
 680                updated = true;
 681            }
 682            _ => {}
 683        };
 684
 685        if updated {
 686            self.refresh_lsp_menu(false, window, cx);
 687        }
 688    }
 689
 690    fn regenerate_items(&mut self, cx: &mut App) {
 691        self.server_state.update(cx, |state, cx| {
 692            let active_worktrees = state
 693                .active_editor
 694                .as_ref()
 695                .into_iter()
 696                .flat_map(|active_editor| {
 697                    active_editor
 698                        .editor
 699                        .upgrade()
 700                        .into_iter()
 701                        .flat_map(|active_editor| {
 702                            active_editor
 703                                .read(cx)
 704                                .buffer()
 705                                .read(cx)
 706                                .all_buffers()
 707                                .into_iter()
 708                                .filter_map(|buffer| {
 709                                    project::File::from_dyn(buffer.read(cx).file())
 710                                })
 711                                .map(|buffer_file| buffer_file.worktree.clone())
 712                        })
 713                })
 714                .collect::<HashSet<_>>();
 715
 716            let mut server_ids_to_worktrees =
 717                HashMap::<LanguageServerId, Entity<Worktree>>::default();
 718            let mut server_names_to_worktrees = HashMap::<
 719                LanguageServerName,
 720                HashSet<(Entity<Worktree>, LanguageServerId)>,
 721            >::default();
 722            for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
 723                if let Some(worktree) = servers_for_path
 724                    .worktree
 725                    .as_ref()
 726                    .and_then(|worktree| worktree.upgrade())
 727                {
 728                    for (server_id, server_name) in &servers_for_path.servers {
 729                        server_ids_to_worktrees.insert(*server_id, worktree.clone());
 730                        if let Some(server_name) = server_name {
 731                            server_names_to_worktrees
 732                                .entry(server_name.clone())
 733                                .or_default()
 734                                .insert((worktree.clone(), *server_id));
 735                        }
 736                    }
 737                }
 738            }
 739            state
 740                .lsp_store
 741                .update(cx, |lsp_store, cx| {
 742                    for (server_id, status) in lsp_store.language_server_statuses() {
 743                        if let Some(worktree) = status.worktree.and_then(|worktree_id| {
 744                            lsp_store
 745                                .worktree_store()
 746                                .read(cx)
 747                                .worktree_for_id(worktree_id, cx)
 748                        }) {
 749                            server_ids_to_worktrees.insert(server_id, worktree.clone());
 750                            server_names_to_worktrees
 751                                .entry(status.name.clone())
 752                                .or_default()
 753                                .insert((worktree, server_id));
 754                        }
 755                    }
 756                })
 757                .ok();
 758
 759            let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
 760            let mut servers_with_health_checks = HashSet::default();
 761
 762            for (server_id, health) in &state.language_servers.health_statuses {
 763                let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
 764                    let worktrees = server_names_to_worktrees.get(&health.name)?;
 765                    worktrees
 766                        .iter()
 767                        .find(|(worktree, _)| active_worktrees.contains(worktree))
 768                        .or_else(|| worktrees.iter().next())
 769                        .map(|(worktree, _)| worktree)
 770                });
 771                servers_with_health_checks.insert(&health.name);
 772                let worktree_name =
 773                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name_str()));
 774
 775                let binary_status = state.language_servers.binary_statuses.get(&health.name);
 776                let server_data = ServerData::WithHealthCheck {
 777                    server_id: *server_id,
 778                    health,
 779                    binary_status,
 780                };
 781                if let Some(worktree_name) = worktree_name {
 782                    servers_per_worktree
 783                        .entry(worktree_name.clone())
 784                        .or_default()
 785                        .push(server_data);
 786                }
 787            }
 788
 789            let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
 790            let mut can_restart_all = state.language_servers.health_statuses.is_empty();
 791            for (server_name, binary_status) in state
 792                .language_servers
 793                .binary_statuses
 794                .iter()
 795                .filter(|(name, _)| !servers_with_health_checks.contains(name))
 796            {
 797                match binary_status.status {
 798                    BinaryStatus::None => {
 799                        can_restart_all = false;
 800                        can_stop_all |= true;
 801                    }
 802                    BinaryStatus::CheckingForUpdate => {
 803                        can_restart_all = false;
 804                        can_stop_all = false;
 805                    }
 806                    BinaryStatus::Downloading => {
 807                        can_restart_all = false;
 808                        can_stop_all = false;
 809                    }
 810                    BinaryStatus::Starting => {
 811                        can_restart_all = false;
 812                        can_stop_all = false;
 813                    }
 814                    BinaryStatus::Stopping => {
 815                        can_restart_all = false;
 816                        can_stop_all = false;
 817                    }
 818                    BinaryStatus::Stopped => {}
 819                    BinaryStatus::Failed { .. } => {}
 820                }
 821
 822                if let Some(worktrees_for_name) = server_names_to_worktrees.get(server_name)
 823                    && let Some((worktree, server_id)) = worktrees_for_name
 824                        .iter()
 825                        .find(|(worktree, _)| active_worktrees.contains(worktree))
 826                        .or_else(|| worktrees_for_name.iter().next())
 827                {
 828                    let worktree_name = SharedString::new(worktree.read(cx).root_name_str());
 829                    servers_per_worktree
 830                        .entry(worktree_name.clone())
 831                        .or_default()
 832                        .push(ServerData::WithBinaryStatus {
 833                            server_name,
 834                            binary_status,
 835                            server_id: *server_id,
 836                        });
 837                }
 838            }
 839
 840            let mut new_lsp_items = Vec::with_capacity(servers_per_worktree.len() + 1);
 841            for (worktree_name, worktree_servers) in servers_per_worktree {
 842                if worktree_servers.is_empty() {
 843                    continue;
 844                }
 845                new_lsp_items.push(LspMenuItem::Header {
 846                    header: Some(worktree_name),
 847                    separator: false,
 848                });
 849                new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
 850            }
 851            if !new_lsp_items.is_empty() {
 852                if can_stop_all {
 853                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
 854                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
 855                } else if can_restart_all {
 856                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
 857                }
 858            }
 859
 860            state.items = new_lsp_items;
 861        });
 862    }
 863
 864    fn refresh_lsp_menu(
 865        &mut self,
 866        create_if_empty: bool,
 867        window: &mut Window,
 868        cx: &mut Context<Self>,
 869    ) {
 870        if create_if_empty || self.lsp_menu.is_some() {
 871            let state = self.server_state.clone();
 872            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
 873                cx.background_executor()
 874                    .timer(Duration::from_millis(30))
 875                    .await;
 876                lsp_button
 877                    .update_in(cx, |lsp_button, window, cx| {
 878                        lsp_button.regenerate_items(cx);
 879                        let menu = ContextMenu::build(window, cx, |menu, _, cx| {
 880                            state.update(cx, |state, cx| state.fill_menu(menu, cx))
 881                        });
 882                        lsp_button.lsp_menu = Some(menu.clone());
 883                        lsp_button.popover_menu_handle.refresh_menu(
 884                            window,
 885                            cx,
 886                            Rc::new(move |_, _| Some(menu.clone())),
 887                        );
 888                        cx.notify();
 889                    })
 890                    .ok();
 891            });
 892        }
 893    }
 894}
 895
 896impl StatusItemView for LspButton {
 897    fn set_active_pane_item(
 898        &mut self,
 899        active_pane_item: Option<&dyn workspace::ItemHandle>,
 900        window: &mut Window,
 901        cx: &mut Context<Self>,
 902    ) {
 903        if ProjectSettings::get_global(cx).global_lsp_settings.button {
 904            if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
 905                if Some(&editor)
 906                    != self
 907                        .server_state
 908                        .read(cx)
 909                        .active_editor
 910                        .as_ref()
 911                        .and_then(|active_editor| active_editor.editor.upgrade())
 912                        .as_ref()
 913                {
 914                    let editor_buffers =
 915                        HashSet::from_iter(editor.read(cx).buffer().read(cx).excerpt_buffer_ids());
 916                    let _editor_subscription = cx.subscribe_in(
 917                        &editor,
 918                        window,
 919                        |lsp_button, _, e: &EditorEvent, window, cx| match e {
 920                            EditorEvent::ExcerptsAdded { buffer, .. } => {
 921                                let updated = lsp_button.server_state.update(cx, |state, cx| {
 922                                    if let Some(active_editor) = state.active_editor.as_mut() {
 923                                        let buffer_id = buffer.read(cx).remote_id();
 924                                        active_editor.editor_buffers.insert(buffer_id)
 925                                    } else {
 926                                        false
 927                                    }
 928                                });
 929                                if updated {
 930                                    lsp_button.refresh_lsp_menu(false, window, cx);
 931                                }
 932                            }
 933                            EditorEvent::ExcerptsRemoved {
 934                                removed_buffer_ids, ..
 935                            } => {
 936                                let removed = lsp_button.server_state.update(cx, |state, _| {
 937                                    let mut removed = false;
 938                                    if let Some(active_editor) = state.active_editor.as_mut() {
 939                                        for id in removed_buffer_ids {
 940                                            active_editor.editor_buffers.retain(|buffer_id| {
 941                                                let retain = buffer_id != id;
 942                                                removed |= !retain;
 943                                                retain
 944                                            });
 945                                        }
 946                                    }
 947                                    removed
 948                                });
 949                                if removed {
 950                                    lsp_button.refresh_lsp_menu(false, window, cx);
 951                                }
 952                            }
 953                            _ => {}
 954                        },
 955                    );
 956                    self.server_state.update(cx, |state, _| {
 957                        state.active_editor = Some(ActiveEditor {
 958                            editor: editor.downgrade(),
 959                            _editor_subscription,
 960                            editor_buffers,
 961                        });
 962                    });
 963                    self.refresh_lsp_menu(true, window, cx);
 964                }
 965            } else if self.server_state.read(cx).active_editor.is_some() {
 966                self.server_state.update(cx, |state, _| {
 967                    state.active_editor = None;
 968                });
 969                self.refresh_lsp_menu(false, window, cx);
 970            }
 971        } else if self.server_state.read(cx).active_editor.is_some() {
 972            self.server_state.update(cx, |state, _| {
 973                state.active_editor = None;
 974            });
 975            self.refresh_lsp_menu(false, window, cx);
 976        }
 977    }
 978}
 979
 980impl Render for LspButton {
 981    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
 982        if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
 983            return div().hidden();
 984        }
 985
 986        let mut has_errors = false;
 987        let mut has_warnings = false;
 988        let mut has_other_notifications = false;
 989        let state = self.server_state.read(cx);
 990        for binary_status in state.language_servers.binary_statuses.values() {
 991            has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
 992            has_other_notifications |= binary_status.message.is_some();
 993        }
 994
 995        for server in state.language_servers.health_statuses.values() {
 996            if let Some((message, health)) = &server.health {
 997                has_other_notifications |= message.is_some();
 998                match health {
 999                    ServerHealth::Ok => {}
1000                    ServerHealth::Warning => has_warnings = true,
1001                    ServerHealth::Error => has_errors = true,
1002                }
1003            }
1004        }
1005
1006        let (indicator, description) = if has_errors {
1007            (
1008                Some(Indicator::dot().color(Color::Error)),
1009                "Server with errors",
1010            )
1011        } else if has_warnings {
1012            (
1013                Some(Indicator::dot().color(Color::Warning)),
1014                "Server with warnings",
1015            )
1016        } else if has_other_notifications {
1017            (
1018                Some(Indicator::dot().color(Color::Modified)),
1019                "Server with notifications",
1020            )
1021        } else {
1022            (None, "All Servers Operational")
1023        };
1024
1025        let lsp_button = cx.weak_entity();
1026
1027        div().child(
1028            PopoverMenu::new("lsp-tool")
1029                .menu(move |_, cx| {
1030                    lsp_button
1031                        .read_with(cx, |lsp_button, _| lsp_button.lsp_menu.clone())
1032                        .ok()
1033                        .flatten()
1034                })
1035                .anchor(Corner::BottomLeft)
1036                .with_handle(self.popover_menu_handle.clone())
1037                .trigger_with_tooltip(
1038                    IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
1039                        .when_some(indicator, IconButton::indicator)
1040                        .icon_size(IconSize::Small)
1041                        .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
1042                    move |_window, cx| {
1043                        Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
1044                    },
1045                ),
1046        )
1047    }
1048}