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