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            let Some(server_info) = item.server_info() else {
189                continue;
190            };
191            let workspace = self.workspace.clone();
192            let server_selector = server_info.server_selector();
193            // TODO currently, Zed remote does not work well with the LSP logs
194            // https://github.com/zed-industries/zed/issues/28557
195            let has_logs = lsp_store.read(cx).as_local().is_some()
196                && lsp_logs.read(cx).has_server_logs(&server_selector);
197            let status_color = server_info
198                .binary_status
199                .and_then(|binary_status| match binary_status.status {
200                    BinaryStatus::None => None,
201                    BinaryStatus::CheckingForUpdate
202                    | BinaryStatus::Downloading
203                    | BinaryStatus::Starting => Some(Color::Modified),
204                    BinaryStatus::Stopping => Some(Color::Disabled),
205                    BinaryStatus::Stopped => Some(Color::Disabled),
206                    BinaryStatus::Failed { .. } => Some(Color::Error),
207                })
208                .or_else(|| {
209                    Some(match server_info.health? {
210                        ServerHealth::Ok => Color::Success,
211                        ServerHealth::Warning => Color::Warning,
212                        ServerHealth::Error => Color::Error,
213                    })
214                })
215                .unwrap_or(Color::Success);
216
217            if self
218                .other_servers_start_index
219                .is_some_and(|index| index == i)
220            {
221                menu = menu.separator();
222            }
223            menu = menu.item(ContextMenuItem::custom_entry(
224                move |_, _| {
225                    h_flex()
226                        .gap_1()
227                        .w_full()
228                        .child(Indicator::dot().color(status_color))
229                        .child(Label::new(server_info.name.0.clone()))
230                        .when(!has_logs, |div| div.cursor_default())
231                        .into_any_element()
232                },
233                {
234                    let lsp_logs = lsp_logs.clone();
235                    move |window, cx| {
236                        if !has_logs {
237                            cx.propagate();
238                            return;
239                        }
240                        lsp_logs.update(cx, |lsp_logs, cx| {
241                            lsp_logs.open_server_trace(
242                                workspace.clone(),
243                                server_selector.clone(),
244                                window,
245                                cx,
246                            );
247                        });
248                    }
249                },
250                server_info.message.map(|server_message| {
251                    DocumentationAside::new(
252                        DocumentationSide::Right,
253                        Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
254                    )
255                }),
256            ));
257        }
258        menu
259    }
260}
261
262impl LanguageServers {
263    fn update_binary_status(
264        &mut self,
265        binary_status: BinaryStatus,
266        message: Option<&str>,
267        name: LanguageServerName,
268    ) {
269        let binary_status_message = message.map(SharedString::new);
270        if matches!(
271            binary_status,
272            BinaryStatus::Stopped | BinaryStatus::Failed { .. }
273        ) {
274            self.health_statuses.retain(|_, server| server.name != name);
275        }
276        self.binary_statuses.insert(
277            name,
278            LanguageServerBinaryStatus {
279                status: binary_status,
280                message: binary_status_message,
281            },
282        );
283    }
284
285    fn update_server_health(
286        &mut self,
287        id: LanguageServerId,
288        health: ServerHealth,
289        message: Option<&str>,
290        name: Option<LanguageServerName>,
291    ) {
292        if let Some(state) = self.health_statuses.get_mut(&id) {
293            state.health = Some((message.map(SharedString::new), health));
294            if let Some(name) = name {
295                state.name = name;
296            }
297        } else if let Some(name) = name {
298            self.health_statuses.insert(
299                id,
300                LanguageServerHealthStatus {
301                    health: Some((message.map(SharedString::new), health)),
302                    name,
303                },
304            );
305        }
306    }
307
308    fn is_empty(&self) -> bool {
309        self.binary_statuses.is_empty() && self.health_statuses.is_empty()
310    }
311}
312
313#[derive(Debug)]
314enum ServerData<'a> {
315    WithHealthCheck(
316        LanguageServerId,
317        &'a LanguageServerHealthStatus,
318        Option<&'a LanguageServerBinaryStatus>,
319    ),
320    WithBinaryStatus(
321        Option<LanguageServerId>,
322        &'a LanguageServerName,
323        &'a LanguageServerBinaryStatus,
324    ),
325}
326
327#[derive(Debug)]
328enum LspItem {
329    WithHealthCheck(
330        LanguageServerId,
331        LanguageServerHealthStatus,
332        Option<LanguageServerBinaryStatus>,
333    ),
334    WithBinaryStatus(
335        Option<LanguageServerId>,
336        LanguageServerName,
337        LanguageServerBinaryStatus,
338    ),
339    ToggleServersButton {
340        restart: bool,
341    },
342}
343
344impl LspItem {
345    fn server_info(&self) -> Option<ServerInfo> {
346        match self {
347            LspItem::ToggleServersButton { .. } => None,
348            LspItem::WithHealthCheck(
349                language_server_id,
350                language_server_health_status,
351                language_server_binary_status,
352            ) => Some(ServerInfo {
353                name: language_server_health_status.name.clone(),
354                id: Some(*language_server_id),
355                health: language_server_health_status.health(),
356                binary_status: language_server_binary_status.clone(),
357                message: language_server_health_status.message(),
358            }),
359            LspItem::WithBinaryStatus(
360                server_id,
361                language_server_name,
362                language_server_binary_status,
363            ) => Some(ServerInfo {
364                name: language_server_name.clone(),
365                id: *server_id,
366                health: None,
367                binary_status: Some(language_server_binary_status.clone()),
368                message: language_server_binary_status.message.clone(),
369            }),
370        }
371    }
372}
373
374impl ServerData<'_> {
375    fn name(&self) -> &LanguageServerName {
376        match self {
377            Self::WithHealthCheck(_, state, _) => &state.name,
378            Self::WithBinaryStatus(_, name, ..) => name,
379        }
380    }
381
382    fn into_lsp_item(self) -> LspItem {
383        match self {
384            Self::WithHealthCheck(id, name, status) => {
385                LspItem::WithHealthCheck(id, name.clone(), status.cloned())
386            }
387            Self::WithBinaryStatus(server_id, name, status) => {
388                LspItem::WithBinaryStatus(server_id, name.clone(), status.clone())
389            }
390        }
391    }
392}
393
394impl LspTool {
395    pub fn new(
396        workspace: &Workspace,
397        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
398        window: &mut Window,
399        cx: &mut Context<Self>,
400    ) -> Self {
401        let settings_subscription =
402            cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
403                if ProjectSettings::get_global(cx).global_lsp_settings.button {
404                    if lsp_tool.lsp_menu.is_none() {
405                        lsp_tool.refresh_lsp_menu(true, window, cx);
406                        return;
407                    }
408                } else if lsp_tool.lsp_menu.take().is_some() {
409                    cx.notify();
410                }
411            });
412
413        let lsp_store = workspace.project().read(cx).lsp_store();
414        let lsp_store_subscription =
415            cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| {
416                lsp_tool.on_lsp_store_event(e, window, cx)
417            });
418
419        let state = cx.new(|_| LanguageServerState {
420            workspace: workspace.weak_handle(),
421            items: Vec::new(),
422            other_servers_start_index: None,
423            lsp_store: lsp_store.downgrade(),
424            active_editor: None,
425            language_servers: LanguageServers::default(),
426        });
427
428        Self {
429            server_state: state,
430            popover_menu_handle,
431            lsp_menu: None,
432            lsp_menu_refresh: Task::ready(()),
433            _subscriptions: vec![settings_subscription, lsp_store_subscription],
434        }
435    }
436
437    fn on_lsp_store_event(
438        &mut self,
439        e: &LspStoreEvent,
440        window: &mut Window,
441        cx: &mut Context<Self>,
442    ) {
443        if self.lsp_menu.is_none() {
444            return;
445        };
446        let mut updated = false;
447
448        match e {
449            LspStoreEvent::LanguageServerUpdate {
450                language_server_id,
451                name,
452                message: proto::update_language_server::Variant::StatusUpdate(status_update),
453            } => match &status_update.status {
454                Some(proto::status_update::Status::Binary(binary_status)) => {
455                    let Some(name) = name.as_ref() else {
456                        return;
457                    };
458                    if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
459                    {
460                        let binary_status = match binary_status {
461                            proto::ServerBinaryStatus::None => BinaryStatus::None,
462                            proto::ServerBinaryStatus::CheckingForUpdate => {
463                                BinaryStatus::CheckingForUpdate
464                            }
465                            proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
466                            proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
467                            proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
468                            proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
469                            proto::ServerBinaryStatus::Failed => {
470                                let Some(error) = status_update.message.clone() else {
471                                    return;
472                                };
473                                BinaryStatus::Failed { error }
474                            }
475                        };
476                        self.server_state.update(cx, |state, _| {
477                            state.language_servers.update_binary_status(
478                                binary_status,
479                                status_update.message.as_deref(),
480                                name.clone(),
481                            );
482                        });
483                        updated = true;
484                    };
485                }
486                Some(proto::status_update::Status::Health(health_status)) => {
487                    if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
488                        let health = match health {
489                            proto::ServerHealth::Ok => ServerHealth::Ok,
490                            proto::ServerHealth::Warning => ServerHealth::Warning,
491                            proto::ServerHealth::Error => ServerHealth::Error,
492                        };
493                        self.server_state.update(cx, |state, _| {
494                            state.language_servers.update_server_health(
495                                *language_server_id,
496                                health,
497                                status_update.message.as_deref(),
498                                name.clone(),
499                            );
500                        });
501                        updated = true;
502                    }
503                }
504                None => {}
505            },
506            LspStoreEvent::LanguageServerUpdate {
507                language_server_id,
508                name,
509                message: proto::update_language_server::Variant::RegisteredForBuffer(update),
510                ..
511            } => {
512                self.server_state.update(cx, |state, _| {
513                    state
514                        .language_servers
515                        .servers_per_buffer_abs_path
516                        .entry(PathBuf::from(&update.buffer_abs_path))
517                        .or_default()
518                        .insert(*language_server_id, name.clone());
519                });
520                updated = true;
521            }
522            _ => {}
523        };
524
525        if updated {
526            self.refresh_lsp_menu(false, window, cx);
527        }
528    }
529
530    fn regenerate_items(&mut self, cx: &mut App) {
531        self.server_state.update(cx, |state, cx| {
532            let editor_buffers = state
533                .active_editor
534                .as_ref()
535                .map(|active_editor| active_editor.editor_buffers.clone())
536                .unwrap_or_default();
537            let editor_buffer_paths = editor_buffers
538                .iter()
539                .filter_map(|buffer_id| {
540                    let buffer_path = state
541                        .lsp_store
542                        .update(cx, |lsp_store, cx| {
543                            Some(
544                                project::File::from_dyn(
545                                    lsp_store
546                                        .buffer_store()
547                                        .read(cx)
548                                        .get(*buffer_id)?
549                                        .read(cx)
550                                        .file(),
551                                )?
552                                .abs_path(cx),
553                            )
554                        })
555                        .ok()??;
556                    Some(buffer_path)
557                })
558                .collect::<Vec<_>>();
559
560            let mut servers_with_health_checks = HashSet::default();
561            let mut server_ids_with_health_checks = HashSet::default();
562            let mut buffer_servers =
563                Vec::with_capacity(state.language_servers.health_statuses.len());
564            let mut other_servers =
565                Vec::with_capacity(state.language_servers.health_statuses.len());
566            let buffer_server_ids = editor_buffer_paths
567                .iter()
568                .filter_map(|buffer_path| {
569                    state
570                        .language_servers
571                        .servers_per_buffer_abs_path
572                        .get(buffer_path)
573                })
574                .flatten()
575                .fold(HashMap::default(), |mut acc, (server_id, name)| {
576                    match acc.entry(*server_id) {
577                        hash_map::Entry::Occupied(mut o) => {
578                            let old_name: &mut Option<&LanguageServerName> = o.get_mut();
579                            if old_name.is_none() {
580                                *old_name = name.as_ref();
581                            }
582                        }
583                        hash_map::Entry::Vacant(v) => {
584                            v.insert(name.as_ref());
585                        }
586                    }
587                    acc
588                });
589            for (server_id, server_state) in &state.language_servers.health_statuses {
590                let binary_status = state
591                    .language_servers
592                    .binary_statuses
593                    .get(&server_state.name);
594                servers_with_health_checks.insert(&server_state.name);
595                server_ids_with_health_checks.insert(*server_id);
596                if buffer_server_ids.contains_key(server_id) {
597                    buffer_servers.push(ServerData::WithHealthCheck(
598                        *server_id,
599                        server_state,
600                        binary_status,
601                    ));
602                } else {
603                    other_servers.push(ServerData::WithHealthCheck(
604                        *server_id,
605                        server_state,
606                        binary_status,
607                    ));
608                }
609            }
610
611            let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
612            let mut can_restart_all = state.language_servers.health_statuses.is_empty();
613            for (server_name, status) in state
614                .language_servers
615                .binary_statuses
616                .iter()
617                .filter(|(name, _)| !servers_with_health_checks.contains(name))
618            {
619                match status.status {
620                    BinaryStatus::None => {
621                        can_restart_all = false;
622                        can_stop_all |= true;
623                    }
624                    BinaryStatus::CheckingForUpdate => {
625                        can_restart_all = false;
626                        can_stop_all = false;
627                    }
628                    BinaryStatus::Downloading => {
629                        can_restart_all = false;
630                        can_stop_all = false;
631                    }
632                    BinaryStatus::Starting => {
633                        can_restart_all = false;
634                        can_stop_all = false;
635                    }
636                    BinaryStatus::Stopping => {
637                        can_restart_all = false;
638                        can_stop_all = false;
639                    }
640                    BinaryStatus::Stopped => {}
641                    BinaryStatus::Failed { .. } => {}
642                }
643
644                let matching_server_id = state
645                    .language_servers
646                    .servers_per_buffer_abs_path
647                    .iter()
648                    .filter(|(path, _)| editor_buffer_paths.contains(path))
649                    .flat_map(|(_, server_associations)| server_associations.iter())
650                    .find_map(|(id, name)| {
651                        if name.as_ref() == Some(server_name) {
652                            Some(*id)
653                        } else {
654                            None
655                        }
656                    });
657                if let Some(server_id) = matching_server_id {
658                    buffer_servers.push(ServerData::WithBinaryStatus(
659                        Some(server_id),
660                        server_name,
661                        status,
662                    ));
663                } else {
664                    other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
665                }
666            }
667
668            buffer_servers.sort_by_key(|data| data.name().clone());
669            other_servers.sort_by_key(|data| data.name().clone());
670
671            let mut other_servers_start_index = None;
672            let mut new_lsp_items =
673                Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
674            new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
675            if !new_lsp_items.is_empty() {
676                other_servers_start_index = Some(new_lsp_items.len());
677            }
678            new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
679            if !new_lsp_items.is_empty() {
680                if can_stop_all {
681                    new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
682                } else if can_restart_all {
683                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
684                }
685            }
686
687            state.items = new_lsp_items;
688            state.other_servers_start_index = other_servers_start_index;
689        });
690    }
691
692    fn refresh_lsp_menu(
693        &mut self,
694        create_if_empty: bool,
695        window: &mut Window,
696        cx: &mut Context<Self>,
697    ) {
698        if create_if_empty || self.lsp_menu.is_some() {
699            let state = self.server_state.clone();
700            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| {
701                cx.background_executor()
702                    .timer(Duration::from_millis(30))
703                    .await;
704                lsp_tool
705                    .update_in(cx, |lsp_tool, window, cx| {
706                        lsp_tool.regenerate_items(cx);
707                        let menu = ContextMenu::build(window, cx, |menu, _, cx| {
708                            state.update(cx, |state, cx| state.fill_menu(menu, cx))
709                        });
710                        lsp_tool.lsp_menu = Some(menu.clone());
711                        // TODO kb will this work?
712                        // what about the selections?
713                        lsp_tool.popover_menu_handle.refresh_menu(
714                            window,
715                            cx,
716                            Rc::new(move |_, _| Some(menu.clone())),
717                        );
718                        cx.notify();
719                    })
720                    .ok();
721            });
722        }
723    }
724}
725
726impl StatusItemView for LspTool {
727    fn set_active_pane_item(
728        &mut self,
729        active_pane_item: Option<&dyn workspace::ItemHandle>,
730        window: &mut Window,
731        cx: &mut Context<Self>,
732    ) {
733        if ProjectSettings::get_global(cx).global_lsp_settings.button {
734            if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
735                if Some(&editor)
736                    != self
737                        .server_state
738                        .read(cx)
739                        .active_editor
740                        .as_ref()
741                        .and_then(|active_editor| active_editor.editor.upgrade())
742                        .as_ref()
743                {
744                    let editor_buffers =
745                        HashSet::from_iter(editor.read(cx).buffer().read(cx).excerpt_buffer_ids());
746                    let _editor_subscription = cx.subscribe_in(
747                        &editor,
748                        window,
749                        |lsp_tool, _, e: &EditorEvent, window, cx| match e {
750                            EditorEvent::ExcerptsAdded { buffer, .. } => {
751                                let updated = lsp_tool.server_state.update(cx, |state, cx| {
752                                    if let Some(active_editor) = state.active_editor.as_mut() {
753                                        let buffer_id = buffer.read(cx).remote_id();
754                                        active_editor.editor_buffers.insert(buffer_id)
755                                    } else {
756                                        false
757                                    }
758                                });
759                                if updated {
760                                    lsp_tool.refresh_lsp_menu(false, window, cx);
761                                }
762                            }
763                            EditorEvent::ExcerptsRemoved {
764                                removed_buffer_ids, ..
765                            } => {
766                                let removed = lsp_tool.server_state.update(cx, |state, _| {
767                                    let mut removed = false;
768                                    if let Some(active_editor) = state.active_editor.as_mut() {
769                                        for id in removed_buffer_ids {
770                                            active_editor.editor_buffers.retain(|buffer_id| {
771                                                let retain = buffer_id != id;
772                                                removed |= !retain;
773                                                retain
774                                            });
775                                        }
776                                    }
777                                    removed
778                                });
779                                if removed {
780                                    lsp_tool.refresh_lsp_menu(false, window, cx);
781                                }
782                            }
783                            _ => {}
784                        },
785                    );
786                    self.server_state.update(cx, |state, _| {
787                        state.active_editor = Some(ActiveEditor {
788                            editor: editor.downgrade(),
789                            _editor_subscription,
790                            editor_buffers,
791                        });
792                    });
793                    self.refresh_lsp_menu(true, window, cx);
794                }
795            } else if self.server_state.read(cx).active_editor.is_some() {
796                self.server_state.update(cx, |state, _| {
797                    state.active_editor = None;
798                });
799                self.refresh_lsp_menu(false, window, cx);
800            }
801        } else if self.server_state.read(cx).active_editor.is_some() {
802            self.server_state.update(cx, |state, _| {
803                state.active_editor = None;
804            });
805            self.refresh_lsp_menu(false, window, cx);
806        }
807    }
808}
809
810impl Render for LspTool {
811    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
812        if !cx.is_staff()
813            || self.server_state.read(cx).language_servers.is_empty()
814            || self.lsp_menu.is_none()
815        {
816            return div();
817        }
818
819        let mut has_errors = false;
820        let mut has_warnings = false;
821        let mut has_other_notifications = false;
822        let state = self.server_state.read(cx);
823        for server in state.language_servers.health_statuses.values() {
824            if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) {
825                has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
826                has_other_notifications |= binary_status.message.is_some();
827            }
828
829            if let Some((message, health)) = &server.health {
830                has_other_notifications |= message.is_some();
831                match health {
832                    ServerHealth::Ok => {}
833                    ServerHealth::Warning => has_warnings = true,
834                    ServerHealth::Error => has_errors = true,
835                }
836            }
837        }
838
839        let indicator = if has_errors {
840            Some(Indicator::dot().color(Color::Error))
841        } else if has_warnings {
842            Some(Indicator::dot().color(Color::Warning))
843        } else if has_other_notifications {
844            Some(Indicator::dot().color(Color::Modified))
845        } else {
846            None
847        };
848
849        let lsp_tool = cx.entity().clone();
850        div().child(
851            PopoverMenu::new("lsp-tool")
852                .menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone())
853                .anchor(Corner::BottomLeft)
854                .with_handle(self.popover_menu_handle.clone())
855                .trigger_with_tooltip(
856                    IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt)
857                        .when_some(indicator, IconButton::indicator)
858                        .icon_size(IconSize::Small)
859                        .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
860                    move |window, cx| {
861                        Tooltip::for_action("Language Servers", &ToggleMenu, window, cx)
862                    },
863                ),
864        )
865    }
866}