lsp_button.rs

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