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