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