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