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