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