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