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