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