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