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