lsp_tool.rs

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