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