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