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