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