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 workspace::{StatusItemView, Workspace};
  25
  26use crate::lsp_log_view;
  27
  28actions!(
  29    lsp_tool,
  30    [
  31        /// Toggles the language server tool menu.
  32        ToggleMenu
  33    ]
  34);
  35
  36pub struct LspButton {
  37    server_state: Entity<LanguageServerState>,
  38    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
  39    lsp_menu: Option<Entity<ContextMenu>>,
  40    lsp_menu_refresh: Task<()>,
  41    _subscriptions: Vec<Subscription>,
  42}
  43
  44#[derive(Debug)]
  45struct LanguageServerState {
  46    items: Vec<LspMenuItem>,
  47    workspace: WeakEntity<Workspace>,
  48    lsp_store: WeakEntity<LspStore>,
  49    active_editor: Option<ActiveEditor>,
  50    language_servers: LanguageServers,
  51}
  52
  53struct ActiveEditor {
  54    editor: WeakEntity<Editor>,
  55    _editor_subscription: Subscription,
  56    editor_buffers: HashSet<BufferId>,
  57}
  58
  59impl std::fmt::Debug for ActiveEditor {
  60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  61        f.debug_struct("ActiveEditor")
  62            .field("editor", &self.editor)
  63            .field("editor_buffers", &self.editor_buffers)
  64            .finish_non_exhaustive()
  65    }
  66}
  67
  68#[derive(Debug, Default, Clone)]
  69struct LanguageServers {
  70    health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
  71    binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
  72    servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
  73}
  74
  75#[derive(Debug, Clone)]
  76struct ServersForPath {
  77    servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
  78    worktree: Option<WeakEntity<Worktree>>,
  79}
  80
  81#[derive(Debug, Clone)]
  82struct LanguageServerHealthStatus {
  83    name: LanguageServerName,
  84    health: Option<(Option<SharedString>, ServerHealth)>,
  85}
  86
  87#[derive(Debug, Clone)]
  88struct LanguageServerBinaryStatus {
  89    status: BinaryStatus,
  90    message: Option<SharedString>,
  91}
  92
  93#[derive(Debug)]
  94struct ServerInfo {
  95    name: LanguageServerName,
  96    id: Option<LanguageServerId>,
  97    health: Option<ServerHealth>,
  98    binary_status: Option<LanguageServerBinaryStatus>,
  99    message: Option<SharedString>,
 100}
 101
 102impl ServerInfo {
 103    fn server_selector(&self) -> LanguageServerSelector {
 104        self.id
 105            .map(LanguageServerSelector::Id)
 106            .unwrap_or_else(|| LanguageServerSelector::Name(self.name.clone()))
 107    }
 108}
 109
 110impl LanguageServerHealthStatus {
 111    fn health(&self) -> Option<ServerHealth> {
 112        self.health.as_ref().map(|(_, health)| *health)
 113    }
 114
 115    fn message(&self) -> Option<SharedString> {
 116        self.health
 117            .as_ref()
 118            .and_then(|(message, _)| message.clone())
 119    }
 120}
 121
 122impl LanguageServerState {
 123    fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
 124        let lsp_logs = cx
 125            .try_global::<GlobalLogStore>()
 126            .map(|lsp_logs| lsp_logs.0.clone());
 127        let Some(lsp_logs) = lsp_logs else {
 128            return menu;
 129        };
 130
 131        let mut first_button_encountered = false;
 132        for item in &self.items {
 133            if let LspMenuItem::ToggleServersButton { restart } = item {
 134                let label = if *restart {
 135                    "Restart All Servers"
 136                } else {
 137                    "Stop All Servers"
 138                };
 139                let restart = *restart;
 140                let button = ContextMenuEntry::new(label).handler({
 141                    let state = cx.entity();
 142                    move |_, cx| {
 143                        let lsp_store = state.read(cx).lsp_store.clone();
 144                        lsp_store
 145                            .update(cx, |lsp_store, cx| {
 146                                if restart {
 147                                    let Some(workspace) = state.read(cx).workspace.upgrade() else {
 148                                        return;
 149                                    };
 150                                    let project = workspace.read(cx).project().clone();
 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 entry = worktree.entry_for_path(&relative_path)?;
 163                                            let project_path =
 164                                                project.read(cx).path_for_entry(entry.id, cx)?;
 165                                            buffer_store.read(cx).get_by_path(&project_path)
 166                                        })
 167                                        .collect();
 168                                    let selectors = state
 169                                        .read(cx)
 170                                        .items
 171                                        .iter()
 172                                        // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
 173                                        .flat_map(|item| match item {
 174                                            LspMenuItem::Header { .. } => None,
 175                                            LspMenuItem::ToggleServersButton { .. } => None,
 176                                            LspMenuItem::WithHealthCheck { health, .. } => Some(
 177                                                LanguageServerSelector::Name(health.name.clone()),
 178                                            ),
 179                                            LspMenuItem::WithBinaryStatus {
 180                                                server_name, ..
 181                                            } => Some(LanguageServerSelector::Name(
 182                                                server_name.clone(),
 183                                            )),
 184                                        })
 185                                        .collect();
 186                                    lsp_store.restart_language_servers_for_buffers(
 187                                        buffers, selectors, cx,
 188                                    );
 189                                } else {
 190                                    lsp_store.stop_all_language_servers(cx);
 191                                }
 192                            })
 193                            .ok();
 194                    }
 195                });
 196                if !first_button_encountered {
 197                    menu = menu.separator();
 198                    first_button_encountered = true;
 199                }
 200                menu = menu.item(button);
 201                continue;
 202            } else if let LspMenuItem::Header { header, separator } = item {
 203                menu = menu
 204                    .when(*separator, |menu| menu.separator())
 205                    .when_some(header.as_ref(), |menu, header| menu.header(header));
 206                continue;
 207            }
 208
 209            let Some(server_info) = item.server_info() else {
 210                continue;
 211            };
 212
 213            let server_selector = server_info.server_selector();
 214            let is_remote = self
 215                .lsp_store
 216                .update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
 217                .unwrap_or(false);
 218            let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
 219
 220            let status_color = server_info
 221                .binary_status
 222                .as_ref()
 223                .and_then(|binary_status| match binary_status.status {
 224                    BinaryStatus::None => None,
 225                    BinaryStatus::CheckingForUpdate
 226                    | BinaryStatus::Downloading
 227                    | BinaryStatus::Starting => Some(Color::Modified),
 228                    BinaryStatus::Stopping => Some(Color::Disabled),
 229                    BinaryStatus::Stopped => Some(Color::Disabled),
 230                    BinaryStatus::Failed { .. } => Some(Color::Error),
 231                })
 232                .or_else(|| {
 233                    Some(match server_info.health? {
 234                        ServerHealth::Ok => Color::Success,
 235                        ServerHealth::Warning => Color::Warning,
 236                        ServerHealth::Error => Color::Error,
 237                    })
 238                })
 239                .unwrap_or(Color::Success);
 240
 241            let message = server_info
 242                .message
 243                .as_ref()
 244                .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
 245                .cloned();
 246            let hover_label = if message.is_some() {
 247                Some("View Message")
 248            } else if has_logs {
 249                Some("View Logs")
 250            } else {
 251                None
 252            };
 253
 254            let server_name = server_info.name.clone();
 255            menu = menu.item(ContextMenuItem::custom_entry(
 256                move |_, _| {
 257                    h_flex()
 258                        .group("menu_item")
 259                        .w_full()
 260                        .gap_2()
 261                        .justify_between()
 262                        .child(
 263                            h_flex()
 264                                .gap_2()
 265                                .child(Indicator::dot().color(status_color))
 266                                .child(Label::new(server_name.0.clone())),
 267                        )
 268                        .when_some(hover_label, |div, hover_label| {
 269                            div.child(
 270                                h_flex()
 271                                    .visible_on_hover("menu_item")
 272                                    .child(
 273                                        Label::new(hover_label)
 274                                            .size(LabelSize::Small)
 275                                            .color(Color::Muted),
 276                                    )
 277                                    .child(
 278                                        Icon::new(IconName::ChevronRight)
 279                                            .size(IconSize::Small)
 280                                            .color(Color::Muted),
 281                                    ),
 282                            )
 283                        })
 284                        .into_any_element()
 285                },
 286                {
 287                    let lsp_logs = lsp_logs.clone();
 288                    let message = message.clone();
 289                    let server_selector = server_selector.clone();
 290                    let server_name = server_info.name.clone();
 291                    let workspace = self.workspace.clone();
 292                    move |window, cx| {
 293                        if let Some(message) = &message {
 294                            let Some(create_buffer) = workspace
 295                                .update(cx, |workspace, cx| {
 296                                    workspace
 297                                        .project()
 298                                        .update(cx, |project, cx| project.create_buffer(false, cx))
 299                                })
 300                                .ok()
 301                            else {
 302                                return;
 303                            };
 304
 305                            let window = window.window_handle();
 306                            let workspace = workspace.clone();
 307                            let message = message.clone();
 308                            let server_name = server_name.clone();
 309                            cx.spawn(async move |cx| {
 310                                let buffer = create_buffer.await?;
 311                                buffer.update(cx, |buffer, cx| {
 312                                    buffer.edit(
 313                                        [(
 314                                            0..0,
 315                                            format!("Language server {server_name}:\n\n{message}"),
 316                                        )],
 317                                        None,
 318                                        cx,
 319                                    );
 320                                    buffer.set_capability(language::Capability::ReadOnly, cx);
 321                                })?;
 322
 323                                workspace.update(cx, |workspace, cx| {
 324                                    window.update(cx, |_, window, cx| {
 325                                        workspace.add_item_to_active_pane(
 326                                            Box::new(cx.new(|cx| {
 327                                                let mut editor =
 328                                                    Editor::for_buffer(buffer, None, window, cx);
 329                                                editor.set_read_only(true);
 330                                                editor
 331                                            })),
 332                                            None,
 333                                            true,
 334                                            window,
 335                                            cx,
 336                                        );
 337                                    })
 338                                })??;
 339
 340                                anyhow::Ok(())
 341                            })
 342                            .detach();
 343                        } else if has_logs {
 344                            lsp_log_view::open_server_trace(
 345                                &lsp_logs,
 346                                workspace.clone(),
 347                                server_selector.clone(),
 348                                window,
 349                                cx,
 350                            );
 351                        } else {
 352                            cx.propagate();
 353                        }
 354                    }
 355                },
 356                message.map(|server_message| {
 357                    DocumentationAside::new(
 358                        DocumentationSide::Right,
 359                        DocumentationEdge::Bottom,
 360                        Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
 361                    )
 362                }),
 363            ));
 364        }
 365        menu
 366    }
 367}
 368
 369impl LanguageServers {
 370    fn update_binary_status(
 371        &mut self,
 372        binary_status: BinaryStatus,
 373        message: Option<&str>,
 374        name: LanguageServerName,
 375    ) {
 376        let binary_status_message = message.map(SharedString::new);
 377        if matches!(
 378            binary_status,
 379            BinaryStatus::Stopped | BinaryStatus::Failed { .. }
 380        ) {
 381            self.health_statuses.retain(|_, server| server.name != name);
 382        }
 383        self.binary_statuses.insert(
 384            name,
 385            LanguageServerBinaryStatus {
 386                status: binary_status,
 387                message: binary_status_message,
 388            },
 389        );
 390    }
 391
 392    fn update_server_health(
 393        &mut self,
 394        id: LanguageServerId,
 395        health: ServerHealth,
 396        message: Option<&str>,
 397        name: Option<LanguageServerName>,
 398    ) {
 399        if let Some(state) = self.health_statuses.get_mut(&id) {
 400            state.health = Some((message.map(SharedString::new), health));
 401            if let Some(name) = name {
 402                state.name = name;
 403            }
 404        } else if let Some(name) = name {
 405            self.health_statuses.insert(
 406                id,
 407                LanguageServerHealthStatus {
 408                    health: Some((message.map(SharedString::new), health)),
 409                    name,
 410                },
 411            );
 412        }
 413    }
 414
 415    fn is_empty(&self) -> bool {
 416        self.binary_statuses.is_empty() && self.health_statuses.is_empty()
 417    }
 418}
 419
 420#[derive(Debug)]
 421enum ServerData<'a> {
 422    WithHealthCheck {
 423        server_id: LanguageServerId,
 424        health: &'a LanguageServerHealthStatus,
 425        binary_status: Option<&'a LanguageServerBinaryStatus>,
 426    },
 427    WithBinaryStatus {
 428        server_id: Option<LanguageServerId>,
 429        server_name: &'a LanguageServerName,
 430        binary_status: &'a LanguageServerBinaryStatus,
 431    },
 432}
 433
 434#[derive(Debug)]
 435enum LspMenuItem {
 436    WithHealthCheck {
 437        server_id: LanguageServerId,
 438        health: LanguageServerHealthStatus,
 439        binary_status: Option<LanguageServerBinaryStatus>,
 440    },
 441    WithBinaryStatus {
 442        server_id: Option<LanguageServerId>,
 443        server_name: LanguageServerName,
 444        binary_status: LanguageServerBinaryStatus,
 445    },
 446    ToggleServersButton {
 447        restart: bool,
 448    },
 449    Header {
 450        header: Option<SharedString>,
 451        separator: bool,
 452    },
 453}
 454
 455impl LspMenuItem {
 456    fn server_info(&self) -> Option<ServerInfo> {
 457        match self {
 458            Self::Header { .. } => None,
 459            Self::ToggleServersButton { .. } => None,
 460            Self::WithHealthCheck {
 461                server_id,
 462                health,
 463                binary_status,
 464                ..
 465            } => Some(ServerInfo {
 466                name: health.name.clone(),
 467                id: Some(*server_id),
 468                health: health.health(),
 469                binary_status: binary_status.clone(),
 470                message: health.message(),
 471            }),
 472            Self::WithBinaryStatus {
 473                server_id,
 474                server_name,
 475                binary_status,
 476                ..
 477            } => Some(ServerInfo {
 478                name: server_name.clone(),
 479                id: *server_id,
 480                health: None,
 481                binary_status: Some(binary_status.clone()),
 482                message: binary_status.message.clone(),
 483            }),
 484        }
 485    }
 486}
 487
 488impl ServerData<'_> {
 489    fn into_lsp_item(self) -> LspMenuItem {
 490        match self {
 491            Self::WithHealthCheck {
 492                server_id,
 493                health,
 494                binary_status,
 495                ..
 496            } => LspMenuItem::WithHealthCheck {
 497                server_id,
 498                health: health.clone(),
 499                binary_status: binary_status.cloned(),
 500            },
 501            Self::WithBinaryStatus {
 502                server_id,
 503                server_name,
 504                binary_status,
 505                ..
 506            } => LspMenuItem::WithBinaryStatus {
 507                server_id,
 508                server_name: server_name.clone(),
 509                binary_status: binary_status.clone(),
 510            },
 511        }
 512    }
 513}
 514
 515impl LspButton {
 516    pub fn new(
 517        workspace: &Workspace,
 518        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
 519        window: &mut Window,
 520        cx: &mut Context<Self>,
 521    ) -> Self {
 522        let settings_subscription =
 523            cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
 524                if ProjectSettings::get_global(cx).global_lsp_settings.button {
 525                    if lsp_button.lsp_menu.is_none() {
 526                        lsp_button.refresh_lsp_menu(true, window, cx);
 527                    }
 528                } else if lsp_button.lsp_menu.take().is_some() {
 529                    cx.notify();
 530                }
 531            });
 532
 533        let lsp_store = workspace.project().read(cx).lsp_store();
 534        let mut language_servers = LanguageServers::default();
 535        for (_, status) in lsp_store.read(cx).language_server_statuses() {
 536            language_servers.binary_statuses.insert(
 537                status.name.clone(),
 538                LanguageServerBinaryStatus {
 539                    status: BinaryStatus::None,
 540                    message: None,
 541                },
 542            );
 543        }
 544
 545        let lsp_store_subscription =
 546            cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
 547                lsp_button.on_lsp_store_event(e, window, cx)
 548            });
 549
 550        let server_state = cx.new(|_| LanguageServerState {
 551            workspace: workspace.weak_handle(),
 552            items: Vec::new(),
 553            lsp_store: lsp_store.downgrade(),
 554            active_editor: None,
 555            language_servers,
 556        });
 557
 558        let mut lsp_button = Self {
 559            server_state,
 560            popover_menu_handle,
 561            lsp_menu: None,
 562            lsp_menu_refresh: Task::ready(()),
 563            _subscriptions: vec![settings_subscription, lsp_store_subscription],
 564        };
 565        if !lsp_button
 566            .server_state
 567            .read(cx)
 568            .language_servers
 569            .binary_statuses
 570            .is_empty()
 571        {
 572            lsp_button.refresh_lsp_menu(true, window, cx);
 573        }
 574
 575        lsp_button
 576    }
 577
 578    fn on_lsp_store_event(
 579        &mut self,
 580        e: &LspStoreEvent,
 581        window: &mut Window,
 582        cx: &mut Context<Self>,
 583    ) {
 584        if self.lsp_menu.is_none() {
 585            return;
 586        };
 587        let mut updated = false;
 588
 589        match e {
 590            LspStoreEvent::LanguageServerUpdate {
 591                language_server_id,
 592                name,
 593                message: proto::update_language_server::Variant::StatusUpdate(status_update),
 594            } => match &status_update.status {
 595                Some(proto::status_update::Status::Binary(binary_status)) => {
 596                    let Some(name) = name.as_ref() else {
 597                        return;
 598                    };
 599                    if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
 600                    {
 601                        let binary_status = match binary_status {
 602                            proto::ServerBinaryStatus::None => BinaryStatus::None,
 603                            proto::ServerBinaryStatus::CheckingForUpdate => {
 604                                BinaryStatus::CheckingForUpdate
 605                            }
 606                            proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
 607                            proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
 608                            proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
 609                            proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
 610                            proto::ServerBinaryStatus::Failed => {
 611                                let Some(error) = status_update.message.clone() else {
 612                                    return;
 613                                };
 614                                BinaryStatus::Failed { error }
 615                            }
 616                        };
 617                        self.server_state.update(cx, |state, _| {
 618                            state.language_servers.update_binary_status(
 619                                binary_status,
 620                                status_update.message.as_deref(),
 621                                name.clone(),
 622                            );
 623                        });
 624                        updated = true;
 625                    };
 626                }
 627                Some(proto::status_update::Status::Health(health_status)) => {
 628                    if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
 629                        let health = match health {
 630                            proto::ServerHealth::Ok => ServerHealth::Ok,
 631                            proto::ServerHealth::Warning => ServerHealth::Warning,
 632                            proto::ServerHealth::Error => ServerHealth::Error,
 633                        };
 634                        self.server_state.update(cx, |state, _| {
 635                            state.language_servers.update_server_health(
 636                                *language_server_id,
 637                                health,
 638                                status_update.message.as_deref(),
 639                                name.clone(),
 640                            );
 641                        });
 642                        updated = true;
 643                    }
 644                }
 645                None => {}
 646            },
 647            LspStoreEvent::LanguageServerUpdate {
 648                language_server_id,
 649                name,
 650                message: proto::update_language_server::Variant::RegisteredForBuffer(update),
 651                ..
 652            } => {
 653                self.server_state.update(cx, |state, cx| {
 654                    let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
 655                        workspace
 656                            .project()
 657                            .read(cx)
 658                            .find_worktree(Path::new(&update.buffer_abs_path), cx)
 659                            .map(|(worktree, _)| worktree.downgrade())
 660                    }) else {
 661                        return;
 662                    };
 663                    let entry = state
 664                        .language_servers
 665                        .servers_per_buffer_abs_path
 666                        .entry(PathBuf::from(&update.buffer_abs_path))
 667                        .or_insert_with(|| ServersForPath {
 668                            servers: HashMap::default(),
 669                            worktree: worktree.clone(),
 670                        });
 671                    entry.servers.insert(*language_server_id, name.clone());
 672                    if worktree.is_some() {
 673                        entry.worktree = worktree;
 674                    }
 675                });
 676                updated = true;
 677            }
 678            _ => {}
 679        };
 680
 681        if updated {
 682            self.refresh_lsp_menu(false, window, cx);
 683        }
 684    }
 685
 686    fn regenerate_items(&mut self, cx: &mut App) {
 687        self.server_state.update(cx, |state, cx| {
 688            let active_worktrees = state
 689                .active_editor
 690                .as_ref()
 691                .into_iter()
 692                .flat_map(|active_editor| {
 693                    active_editor
 694                        .editor
 695                        .upgrade()
 696                        .into_iter()
 697                        .flat_map(|active_editor| {
 698                            active_editor
 699                                .read(cx)
 700                                .buffer()
 701                                .read(cx)
 702                                .all_buffers()
 703                                .into_iter()
 704                                .filter_map(|buffer| {
 705                                    project::File::from_dyn(buffer.read(cx).file())
 706                                })
 707                                .map(|buffer_file| buffer_file.worktree.clone())
 708                        })
 709                })
 710                .collect::<HashSet<_>>();
 711
 712            let mut server_ids_to_worktrees =
 713                HashMap::<LanguageServerId, Entity<Worktree>>::default();
 714            let mut server_names_to_worktrees = HashMap::<
 715                LanguageServerName,
 716                HashSet<(Entity<Worktree>, LanguageServerId)>,
 717            >::default();
 718            for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
 719                if let Some(worktree) = servers_for_path
 720                    .worktree
 721                    .as_ref()
 722                    .and_then(|worktree| worktree.upgrade())
 723                {
 724                    for (server_id, server_name) in &servers_for_path.servers {
 725                        server_ids_to_worktrees.insert(*server_id, worktree.clone());
 726                        if let Some(server_name) = server_name {
 727                            server_names_to_worktrees
 728                                .entry(server_name.clone())
 729                                .or_default()
 730                                .insert((worktree.clone(), *server_id));
 731                        }
 732                    }
 733                }
 734            }
 735            state
 736                .lsp_store
 737                .update(cx, |lsp_store, cx| {
 738                    for (server_id, status) in lsp_store.language_server_statuses() {
 739                        if let Some(worktree) = status.worktree.and_then(|worktree_id| {
 740                            lsp_store
 741                                .worktree_store()
 742                                .read(cx)
 743                                .worktree_for_id(worktree_id, cx)
 744                        }) {
 745                            server_ids_to_worktrees.insert(server_id, worktree.clone());
 746                            server_names_to_worktrees
 747                                .entry(status.name.clone())
 748                                .or_default()
 749                                .insert((worktree, server_id));
 750                        }
 751                    }
 752                })
 753                .ok();
 754
 755            let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
 756            let mut servers_without_worktree = Vec::<ServerData>::new();
 757            let mut servers_with_health_checks = HashSet::default();
 758
 759            for (server_id, health) in &state.language_servers.health_statuses {
 760                let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
 761                    let worktrees = server_names_to_worktrees.get(&health.name)?;
 762                    worktrees
 763                        .iter()
 764                        .find(|(worktree, _)| active_worktrees.contains(worktree))
 765                        .or_else(|| worktrees.iter().next())
 766                        .map(|(worktree, _)| worktree)
 767                });
 768                servers_with_health_checks.insert(&health.name);
 769                let worktree_name =
 770                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name()));
 771
 772                let binary_status = state.language_servers.binary_statuses.get(&health.name);
 773                let server_data = ServerData::WithHealthCheck {
 774                    server_id: *server_id,
 775                    health,
 776                    binary_status,
 777                };
 778                match worktree_name {
 779                    Some(worktree_name) => servers_per_worktree
 780                        .entry(worktree_name.clone())
 781                        .or_default()
 782                        .push(server_data),
 783                    None => servers_without_worktree.push(server_data),
 784                }
 785            }
 786
 787            let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
 788            let mut can_restart_all = state.language_servers.health_statuses.is_empty();
 789            for (server_name, binary_status) in state
 790                .language_servers
 791                .binary_statuses
 792                .iter()
 793                .filter(|(name, _)| !servers_with_health_checks.contains(name))
 794            {
 795                match binary_status.status {
 796                    BinaryStatus::None => {
 797                        can_restart_all = false;
 798                        can_stop_all |= true;
 799                    }
 800                    BinaryStatus::CheckingForUpdate => {
 801                        can_restart_all = false;
 802                        can_stop_all = false;
 803                    }
 804                    BinaryStatus::Downloading => {
 805                        can_restart_all = false;
 806                        can_stop_all = false;
 807                    }
 808                    BinaryStatus::Starting => {
 809                        can_restart_all = false;
 810                        can_stop_all = false;
 811                    }
 812                    BinaryStatus::Stopping => {
 813                        can_restart_all = false;
 814                        can_stop_all = false;
 815                    }
 816                    BinaryStatus::Stopped => {}
 817                    BinaryStatus::Failed { .. } => {}
 818                }
 819
 820                match server_names_to_worktrees.get(server_name) {
 821                    Some(worktrees_for_name) => {
 822                        match worktrees_for_name
 823                            .iter()
 824                            .find(|(worktree, _)| active_worktrees.contains(worktree))
 825                            .or_else(|| worktrees_for_name.iter().next())
 826                        {
 827                            Some((worktree, server_id)) => {
 828                                let worktree_name =
 829                                    SharedString::new(worktree.read(cx).root_name());
 830                                servers_per_worktree
 831                                    .entry(worktree_name.clone())
 832                                    .or_default()
 833                                    .push(ServerData::WithBinaryStatus {
 834                                        server_name,
 835                                        binary_status,
 836                                        server_id: Some(*server_id),
 837                                    });
 838                            }
 839                            None => servers_without_worktree.push(ServerData::WithBinaryStatus {
 840                                server_name,
 841                                binary_status,
 842                                server_id: None,
 843                            }),
 844                        }
 845                    }
 846                    None => servers_without_worktree.push(ServerData::WithBinaryStatus {
 847                        server_name,
 848                        binary_status,
 849                        server_id: None,
 850                    }),
 851                }
 852            }
 853
 854            let mut new_lsp_items =
 855                Vec::with_capacity(servers_per_worktree.len() + servers_without_worktree.len() + 2);
 856            for (worktree_name, worktree_servers) in servers_per_worktree {
 857                if worktree_servers.is_empty() {
 858                    continue;
 859                }
 860                new_lsp_items.push(LspMenuItem::Header {
 861                    header: Some(worktree_name),
 862                    separator: false,
 863                });
 864                new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
 865            }
 866            if !servers_without_worktree.is_empty() {
 867                new_lsp_items.push(LspMenuItem::Header {
 868                    header: Some(SharedString::from("Unknown worktree")),
 869                    separator: false,
 870                });
 871                new_lsp_items.extend(
 872                    servers_without_worktree
 873                        .into_iter()
 874                        .map(ServerData::into_lsp_item),
 875                );
 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();
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.entity();
1052
1053        div().child(
1054            PopoverMenu::new("lsp-tool")
1055                .menu(move |_, cx| lsp_button.read(cx).lsp_menu.clone())
1056                .anchor(Corner::BottomLeft)
1057                .with_handle(self.popover_menu_handle.clone())
1058                .trigger_with_tooltip(
1059                    IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
1060                        .when_some(indicator, IconButton::indicator)
1061                        .icon_size(IconSize::Small)
1062                        .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
1063                    move |window, cx| {
1064                        Tooltip::with_meta(
1065                            "Language Servers",
1066                            Some(&ToggleMenu),
1067                            description,
1068                            window,
1069                            cx,
1070                        )
1071                    },
1072                ),
1073        )
1074    }
1075}