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