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