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