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