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