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