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