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}