lsp_tool.rs

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