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 let mut visited = HashSet::default();
129 while current != root_pid {
130 if !visited.insert(current) {
131 return false;
132 }
133 match parent_map.get(¤t) {
134 Some(&parent) => current = parent,
135 None => return false,
136 }
137 }
138 true
139 }
140}
141
142struct ActiveEditor {
143 editor: WeakEntity<Editor>,
144 _editor_subscription: Subscription,
145 editor_buffers: HashSet<BufferId>,
146}
147
148impl std::fmt::Debug for ActiveEditor {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("ActiveEditor")
151 .field("editor", &self.editor)
152 .field("editor_buffers", &self.editor_buffers)
153 .finish_non_exhaustive()
154 }
155}
156
157#[derive(Debug, Default, Clone)]
158struct LanguageServers {
159 health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
160 binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
161 servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
162}
163
164#[derive(Debug, Clone)]
165struct ServersForPath {
166 servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
167 worktree: Option<WeakEntity<Worktree>>,
168}
169
170#[derive(Debug, Clone)]
171struct LanguageServerHealthStatus {
172 name: LanguageServerName,
173 health: Option<(Option<SharedString>, ServerHealth)>,
174}
175
176#[derive(Debug, Clone)]
177struct LanguageServerBinaryStatus {
178 status: BinaryStatus,
179 message: Option<SharedString>,
180}
181
182#[derive(Debug, Clone)]
183struct ServerInfo {
184 name: LanguageServerName,
185 id: LanguageServerId,
186 health: Option<ServerHealth>,
187 binary_status: Option<LanguageServerBinaryStatus>,
188 message: Option<SharedString>,
189}
190
191impl ServerInfo {
192 fn server_selector(&self) -> LanguageServerSelector {
193 LanguageServerSelector::Id(self.id)
194 }
195
196 fn can_stop(&self) -> bool {
197 self.binary_status.as_ref().is_none_or(|status| {
198 matches!(status.status, BinaryStatus::None | BinaryStatus::Starting)
199 })
200 }
201}
202
203impl LanguageServerHealthStatus {
204 fn health(&self) -> Option<ServerHealth> {
205 self.health.as_ref().map(|(_, health)| *health)
206 }
207
208 fn message(&self) -> Option<SharedString> {
209 self.health
210 .as_ref()
211 .and_then(|(message, _)| message.clone())
212 }
213}
214
215impl LanguageServerState {
216 fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
217 let lsp_logs = cx
218 .try_global::<GlobalLogStore>()
219 .map(|lsp_logs| lsp_logs.0.clone());
220 let Some(lsp_logs) = lsp_logs else {
221 return menu;
222 };
223
224 let server_metadata = self
225 .lsp_store
226 .update(cx, |lsp_store, _| {
227 lsp_store
228 .language_server_statuses()
229 .map(|(server_id, status)| {
230 (
231 server_id,
232 (
233 status.server_readable_version.clone(),
234 status.binary.as_ref().map(|b| b.path.clone()),
235 status.process_id,
236 ),
237 )
238 })
239 .collect::<HashMap<_, _>>()
240 })
241 .unwrap_or_default();
242
243 let process_memory_cache = self.process_memory_cache.clone();
244
245 let mut first_button_encountered = false;
246 for item in &self.items {
247 if let LspMenuItem::ToggleServersButton { restart } = item {
248 let label = if *restart {
249 "Restart All Servers"
250 } else {
251 "Stop All Servers"
252 };
253
254 let restart = *restart;
255
256 let button = ContextMenuEntry::new(label).handler({
257 let state = cx.entity();
258 move |_, cx| {
259 let lsp_store = state.read(cx).lsp_store.clone();
260 lsp_store
261 .update(cx, |lsp_store, cx| {
262 if restart {
263 lsp_store.restart_all_language_servers(cx);
264 } else {
265 lsp_store.stop_all_language_servers(cx);
266 }
267 })
268 .ok();
269 }
270 });
271
272 if !first_button_encountered {
273 menu = menu.separator();
274 first_button_encountered = true;
275 }
276
277 menu = menu.item(button);
278 continue;
279 } else if let LspMenuItem::Header { header, separator } = item {
280 menu = menu
281 .when(*separator, |menu| menu.separator())
282 .when_some(header.as_ref(), |menu, header| menu.header(header));
283 continue;
284 }
285
286 let Some(server_info) = item.server_info() else {
287 continue;
288 };
289 let server_selector = server_info.server_selector();
290 let is_remote = self
291 .lsp_store
292 .update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
293 .unwrap_or(false);
294 let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
295
296 let (status_color, status_label) = server_info
297 .binary_status
298 .as_ref()
299 .and_then(|binary_status| match binary_status.status {
300 BinaryStatus::None => None,
301 BinaryStatus::CheckingForUpdate
302 | BinaryStatus::Downloading
303 | BinaryStatus::Starting => Some((Color::Modified, "Starting…")),
304 BinaryStatus::Stopping | BinaryStatus::Stopped => {
305 Some((Color::Disabled, "Stopped"))
306 }
307 BinaryStatus::Failed { .. } => Some((Color::Error, "Error")),
308 })
309 .or_else(|| {
310 Some(match server_info.health? {
311 ServerHealth::Ok => (Color::Success, "Running"),
312 ServerHealth::Warning => (Color::Warning, "Warning"),
313 ServerHealth::Error => (Color::Error, "Error"),
314 })
315 })
316 .unwrap_or((Color::Success, "Running"));
317
318 let message = server_info
319 .message
320 .as_ref()
321 .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
322 .cloned();
323
324 let (server_version, binary_path, process_id) = server_metadata
325 .get(&server_info.id)
326 .map(|(version, path, process_id)| {
327 (
328 version.clone(),
329 path.as_ref()
330 .map(|p| SharedString::from(p.compact().to_string_lossy().to_string())),
331 *process_id,
332 )
333 })
334 .unwrap_or((None, None, None));
335
336 let server_message = message.clone();
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 true,
516 cx,
517 );
518 })
519 .ok();
520 }
521 });
522
523 if can_stop {
524 let lsp_store_for_stop = lsp_store.clone();
525 let server_selector_for_stop = server_selector.clone();
526
527 submenu = submenu.entry("Stop Server", None, move |_window, cx| {
528 lsp_store_for_stop
529 .update(cx, |lsp_store, cx| {
530 lsp_store
531 .stop_language_servers_for_buffers(
532 Vec::new(),
533 HashSet::from_iter([
534 server_selector_for_stop.clone()
535 ]),
536 cx,
537 )
538 .detach_and_log_err(cx);
539 })
540 .ok();
541 });
542 }
543
544 submenu = submenu.separator().custom_row({
545 let binary_path = binary_path.clone();
546 let server_version = server_version.clone();
547 let server_message = server_message.clone();
548 let process_memory_cache = process_memory_cache.clone();
549 move |_, cx| {
550 let memory_usage = process_id.map(|pid| {
551 process_memory_cache.borrow_mut().get_memory_usage(pid)
552 });
553
554 let memory_label = memory_usage.map(|bytes| {
555 if bytes >= 1024 * 1024 * 1024 {
556 format!(
557 "{:.1} GB",
558 bytes as f64 / (1024.0 * 1024.0 * 1024.0)
559 )
560 } else {
561 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
562 }
563 });
564
565 let version_label =
566 server_version.as_ref().map(|v| format!("v{}", v.as_ref()));
567
568 let separator_color =
569 cx.theme().colors().icon_disabled.opacity(0.8);
570
571 v_flex()
572 .id("metadata-container")
573 .gap_1()
574 .when_some(server_message.as_ref(), |this, _| {
575 this.w(rems_from_px(240.))
576 })
577 .child(
578 h_flex()
579 .ml_neg_1()
580 .gap_1()
581 .child(
582 Icon::new(IconName::Circle)
583 .color(status_color)
584 .size(IconSize::Small),
585 )
586 .child(
587 Label::new(status_label)
588 .size(LabelSize::Small)
589 .color(Color::Muted),
590 )
591 .when_some(version_label.as_ref(), |row, version| {
592 row.child(
593 Icon::new(IconName::Dash)
594 .color(Color::Custom(separator_color))
595 .size(IconSize::XSmall),
596 )
597 .child(
598 Label::new(version)
599 .size(LabelSize::Small)
600 .color(Color::Muted),
601 )
602 })
603 .when_some(memory_label.as_ref(), |row, memory| {
604 row.child(
605 Icon::new(IconName::Dash)
606 .color(Color::Custom(separator_color))
607 .size(IconSize::XSmall),
608 )
609 .child(
610 Label::new(memory)
611 .size(LabelSize::Small)
612 .color(Color::Muted),
613 )
614 }),
615 )
616 .when_some(server_message.clone(), |container, message| {
617 container.child(
618 Label::new(message)
619 .color(Color::Muted)
620 .size(LabelSize::Small),
621 )
622 })
623 .when_some(binary_path.clone(), |el, path| {
624 el.tooltip(Tooltip::text(path))
625 })
626 .into_any_element()
627 }
628 });
629
630 submenu
631 }
632 },
633 );
634 }
635 menu
636 }
637}
638
639impl LanguageServers {
640 fn update_binary_status(
641 &mut self,
642 binary_status: BinaryStatus,
643 message: Option<&str>,
644 name: LanguageServerName,
645 ) {
646 let binary_status_message = message.map(SharedString::new);
647 if matches!(
648 binary_status,
649 BinaryStatus::Stopped | BinaryStatus::Failed { .. }
650 ) {
651 self.health_statuses.retain(|_, server| server.name != name);
652 }
653 self.binary_statuses.insert(
654 name,
655 LanguageServerBinaryStatus {
656 status: binary_status,
657 message: binary_status_message,
658 },
659 );
660 }
661
662 fn update_server_health(
663 &mut self,
664 id: LanguageServerId,
665 health: ServerHealth,
666 message: Option<&str>,
667 name: Option<LanguageServerName>,
668 ) {
669 if let Some(state) = self.health_statuses.get_mut(&id) {
670 state.health = Some((message.map(SharedString::new), health));
671 if let Some(name) = name {
672 state.name = name;
673 }
674 } else if let Some(name) = name {
675 self.health_statuses.insert(
676 id,
677 LanguageServerHealthStatus {
678 health: Some((message.map(SharedString::new), health)),
679 name,
680 },
681 );
682 }
683 }
684
685 fn is_empty(&self) -> bool {
686 self.binary_statuses.is_empty() && self.health_statuses.is_empty()
687 }
688}
689
690#[derive(Debug)]
691enum ServerData<'a> {
692 WithHealthCheck {
693 server_id: LanguageServerId,
694 health: &'a LanguageServerHealthStatus,
695 binary_status: Option<&'a LanguageServerBinaryStatus>,
696 },
697 WithBinaryStatus {
698 server_id: LanguageServerId,
699 server_name: &'a LanguageServerName,
700 binary_status: &'a LanguageServerBinaryStatus,
701 },
702}
703
704#[derive(Debug)]
705enum LspMenuItem {
706 WithHealthCheck {
707 server_id: LanguageServerId,
708 health: LanguageServerHealthStatus,
709 binary_status: Option<LanguageServerBinaryStatus>,
710 },
711 WithBinaryStatus {
712 server_id: LanguageServerId,
713 server_name: LanguageServerName,
714 binary_status: LanguageServerBinaryStatus,
715 },
716 ToggleServersButton {
717 restart: bool,
718 },
719 Header {
720 header: Option<SharedString>,
721 separator: bool,
722 },
723}
724
725impl LspMenuItem {
726 fn server_info(&self) -> Option<ServerInfo> {
727 match self {
728 Self::Header { .. } => None,
729 Self::ToggleServersButton { .. } => None,
730 Self::WithHealthCheck {
731 server_id,
732 health,
733 binary_status,
734 ..
735 } => Some(ServerInfo {
736 name: health.name.clone(),
737 id: *server_id,
738 health: health.health(),
739 binary_status: binary_status.clone(),
740 message: health.message(),
741 }),
742 Self::WithBinaryStatus {
743 server_id,
744 server_name,
745 binary_status,
746 ..
747 } => Some(ServerInfo {
748 name: server_name.clone(),
749 id: *server_id,
750 health: None,
751 binary_status: Some(binary_status.clone()),
752 message: binary_status.message.clone(),
753 }),
754 }
755 }
756}
757
758impl ServerData<'_> {
759 fn into_lsp_item(self) -> LspMenuItem {
760 match self {
761 Self::WithHealthCheck {
762 server_id,
763 health,
764 binary_status,
765 ..
766 } => LspMenuItem::WithHealthCheck {
767 server_id,
768 health: health.clone(),
769 binary_status: binary_status.cloned(),
770 },
771 Self::WithBinaryStatus {
772 server_id,
773 server_name,
774 binary_status,
775 ..
776 } => LspMenuItem::WithBinaryStatus {
777 server_id,
778 server_name: server_name.clone(),
779 binary_status: binary_status.clone(),
780 },
781 }
782 }
783}
784
785impl LspButton {
786 pub fn new(
787 workspace: &Workspace,
788 popover_menu_handle: PopoverMenuHandle<ContextMenu>,
789 window: &mut Window,
790 cx: &mut Context<Self>,
791 ) -> Self {
792 let settings_subscription =
793 cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
794 if ProjectSettings::get_global(cx).global_lsp_settings.button {
795 if lsp_button.lsp_menu.is_none() {
796 lsp_button.refresh_lsp_menu(true, window, cx);
797 }
798 } else if lsp_button.lsp_menu.take().is_some() {
799 cx.notify();
800 }
801 });
802
803 let lsp_store = workspace.project().read(cx).lsp_store();
804 let mut language_servers = LanguageServers::default();
805 for (_, status) in lsp_store.read(cx).language_server_statuses() {
806 language_servers.binary_statuses.insert(
807 status.name.clone(),
808 LanguageServerBinaryStatus {
809 status: BinaryStatus::None,
810 message: None,
811 },
812 );
813 }
814
815 let lsp_store_subscription =
816 cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
817 lsp_button.on_lsp_store_event(e, window, cx)
818 });
819
820 let server_state = cx.new(|_| LanguageServerState {
821 workspace: workspace.weak_handle(),
822 items: Vec::new(),
823 lsp_store: lsp_store.downgrade(),
824 active_editor: None,
825 language_servers,
826 process_memory_cache: Rc::new(RefCell::new(ProcessMemoryCache::new())),
827 });
828
829 let mut lsp_button = Self {
830 server_state,
831 popover_menu_handle,
832 lsp_menu: None,
833 lsp_menu_refresh: Task::ready(()),
834 _subscriptions: vec![settings_subscription, lsp_store_subscription],
835 };
836 if !lsp_button
837 .server_state
838 .read(cx)
839 .language_servers
840 .binary_statuses
841 .is_empty()
842 {
843 lsp_button.refresh_lsp_menu(true, window, cx);
844 }
845
846 lsp_button
847 }
848
849 fn on_lsp_store_event(
850 &mut self,
851 e: &LspStoreEvent,
852 window: &mut Window,
853 cx: &mut Context<Self>,
854 ) {
855 if self.lsp_menu.is_none() {
856 return;
857 };
858 let mut updated = false;
859
860 // TODO `LspStore` is global and reports status from all language servers, even from the other windows.
861 // Also, we do not get "LSP removed" events so LSPs are never removed.
862 match e {
863 LspStoreEvent::LanguageServerUpdate {
864 language_server_id,
865 name,
866 message: proto::update_language_server::Variant::StatusUpdate(status_update),
867 } => match &status_update.status {
868 Some(proto::status_update::Status::Binary(binary_status)) => {
869 let Some(name) = name.as_ref() else {
870 return;
871 };
872 if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
873 {
874 let binary_status = match binary_status {
875 proto::ServerBinaryStatus::None => BinaryStatus::None,
876 proto::ServerBinaryStatus::CheckingForUpdate => {
877 BinaryStatus::CheckingForUpdate
878 }
879 proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
880 proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
881 proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
882 proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
883 proto::ServerBinaryStatus::Failed => {
884 let Some(error) = status_update.message.clone() else {
885 return;
886 };
887 BinaryStatus::Failed { error }
888 }
889 };
890 self.server_state.update(cx, |state, _| {
891 state.language_servers.update_binary_status(
892 binary_status,
893 status_update.message.as_deref(),
894 name.clone(),
895 );
896 });
897 updated = true;
898 };
899 }
900 Some(proto::status_update::Status::Health(health_status)) => {
901 if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
902 let health = match health {
903 proto::ServerHealth::Ok => ServerHealth::Ok,
904 proto::ServerHealth::Warning => ServerHealth::Warning,
905 proto::ServerHealth::Error => ServerHealth::Error,
906 };
907 self.server_state.update(cx, |state, _| {
908 state.language_servers.update_server_health(
909 *language_server_id,
910 health,
911 status_update.message.as_deref(),
912 name.clone(),
913 );
914 });
915 updated = true;
916 }
917 }
918 None => {}
919 },
920 LspStoreEvent::LanguageServerUpdate {
921 language_server_id,
922 name,
923 message: proto::update_language_server::Variant::RegisteredForBuffer(update),
924 ..
925 } => {
926 self.server_state.update(cx, |state, cx| {
927 let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
928 workspace
929 .project()
930 .read(cx)
931 .find_worktree(Path::new(&update.buffer_abs_path), cx)
932 .map(|(worktree, _)| worktree.downgrade())
933 }) else {
934 return;
935 };
936 let entry = state
937 .language_servers
938 .servers_per_buffer_abs_path
939 .entry(PathBuf::from(&update.buffer_abs_path))
940 .or_insert_with(|| ServersForPath {
941 servers: HashMap::default(),
942 worktree: worktree.clone(),
943 });
944 entry.servers.insert(*language_server_id, name.clone());
945 if worktree.is_some() {
946 entry.worktree = worktree;
947 }
948 });
949 updated = true;
950 }
951 _ => {}
952 };
953
954 if updated {
955 self.refresh_lsp_menu(false, window, cx);
956 }
957 }
958
959 fn regenerate_items(&mut self, cx: &mut App) {
960 self.server_state.update(cx, |state, cx| {
961 let active_worktrees = state
962 .active_editor
963 .as_ref()
964 .into_iter()
965 .flat_map(|active_editor| {
966 active_editor
967 .editor
968 .upgrade()
969 .into_iter()
970 .flat_map(|active_editor| {
971 active_editor
972 .read(cx)
973 .buffer()
974 .read(cx)
975 .all_buffers()
976 .into_iter()
977 .filter_map(|buffer| {
978 project::File::from_dyn(buffer.read(cx).file())
979 })
980 .map(|buffer_file| buffer_file.worktree.clone())
981 })
982 })
983 .collect::<HashSet<_>>();
984
985 let mut server_ids_to_worktrees =
986 HashMap::<LanguageServerId, Entity<Worktree>>::default();
987 let mut server_names_to_worktrees = HashMap::<
988 LanguageServerName,
989 HashSet<(Entity<Worktree>, LanguageServerId)>,
990 >::default();
991 for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
992 if let Some(worktree) = servers_for_path
993 .worktree
994 .as_ref()
995 .and_then(|worktree| worktree.upgrade())
996 {
997 for (server_id, server_name) in &servers_for_path.servers {
998 server_ids_to_worktrees.insert(*server_id, worktree.clone());
999 if let Some(server_name) = server_name {
1000 server_names_to_worktrees
1001 .entry(server_name.clone())
1002 .or_default()
1003 .insert((worktree.clone(), *server_id));
1004 }
1005 }
1006 }
1007 }
1008 state
1009 .lsp_store
1010 .update(cx, |lsp_store, cx| {
1011 for (server_id, status) in lsp_store.language_server_statuses() {
1012 if let Some(worktree) = status.worktree.and_then(|worktree_id| {
1013 lsp_store
1014 .worktree_store()
1015 .read(cx)
1016 .worktree_for_id(worktree_id, cx)
1017 }) {
1018 server_ids_to_worktrees.insert(server_id, worktree.clone());
1019 server_names_to_worktrees
1020 .entry(status.name.clone())
1021 .or_default()
1022 .insert((worktree, server_id));
1023 }
1024 }
1025 })
1026 .ok();
1027
1028 let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
1029 let mut servers_with_health_checks = HashSet::default();
1030
1031 for (server_id, health) in &state.language_servers.health_statuses {
1032 let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
1033 let worktrees = server_names_to_worktrees.get(&health.name)?;
1034 worktrees
1035 .iter()
1036 .find(|(worktree, _)| active_worktrees.contains(worktree))
1037 .or_else(|| worktrees.iter().next())
1038 .map(|(worktree, _)| worktree)
1039 });
1040 servers_with_health_checks.insert(&health.name);
1041 let worktree_name =
1042 worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name_str()));
1043
1044 let binary_status = state.language_servers.binary_statuses.get(&health.name);
1045 let server_data = ServerData::WithHealthCheck {
1046 server_id: *server_id,
1047 health,
1048 binary_status,
1049 };
1050 if let Some(worktree_name) = worktree_name {
1051 servers_per_worktree
1052 .entry(worktree_name.clone())
1053 .or_default()
1054 .push(server_data);
1055 }
1056 }
1057
1058 let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
1059 let mut can_restart_all = state.language_servers.health_statuses.is_empty();
1060 for (server_name, binary_status) in state
1061 .language_servers
1062 .binary_statuses
1063 .iter()
1064 .filter(|(name, _)| !servers_with_health_checks.contains(name))
1065 {
1066 match binary_status.status {
1067 BinaryStatus::None => {
1068 can_restart_all = false;
1069 can_stop_all |= true;
1070 }
1071 BinaryStatus::CheckingForUpdate => {
1072 can_restart_all = false;
1073 can_stop_all = false;
1074 }
1075 BinaryStatus::Downloading => {
1076 can_restart_all = false;
1077 can_stop_all = false;
1078 }
1079 BinaryStatus::Starting => {
1080 can_restart_all = false;
1081 can_stop_all = false;
1082 }
1083 BinaryStatus::Stopping => {
1084 can_restart_all = false;
1085 can_stop_all = false;
1086 }
1087 BinaryStatus::Stopped => {}
1088 BinaryStatus::Failed { .. } => {}
1089 }
1090
1091 if let Some(worktrees_for_name) = server_names_to_worktrees.get(server_name)
1092 && let Some((worktree, server_id)) = worktrees_for_name
1093 .iter()
1094 .find(|(worktree, _)| active_worktrees.contains(worktree))
1095 .or_else(|| worktrees_for_name.iter().next())
1096 {
1097 let worktree_name = SharedString::new(worktree.read(cx).root_name_str());
1098 servers_per_worktree
1099 .entry(worktree_name.clone())
1100 .or_default()
1101 .push(ServerData::WithBinaryStatus {
1102 server_name,
1103 binary_status,
1104 server_id: *server_id,
1105 });
1106 }
1107 }
1108
1109 let mut new_lsp_items = Vec::with_capacity(servers_per_worktree.len() + 1);
1110 for (worktree_name, worktree_servers) in servers_per_worktree {
1111 if worktree_servers.is_empty() {
1112 continue;
1113 }
1114 new_lsp_items.push(LspMenuItem::Header {
1115 header: Some(worktree_name),
1116 separator: false,
1117 });
1118 new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
1119 }
1120 if !new_lsp_items.is_empty() {
1121 if can_stop_all {
1122 new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1123 new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
1124 } else if can_restart_all {
1125 new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1126 }
1127 }
1128
1129 state.items = new_lsp_items;
1130 });
1131 }
1132
1133 fn refresh_lsp_menu(
1134 &mut self,
1135 create_if_empty: bool,
1136 window: &mut Window,
1137 cx: &mut Context<Self>,
1138 ) {
1139 if create_if_empty || self.lsp_menu.is_some() {
1140 let state = self.server_state.clone();
1141 self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
1142 cx.background_executor()
1143 .timer(Duration::from_millis(30))
1144 .await;
1145 lsp_button
1146 .update_in(cx, |lsp_button, window, cx| {
1147 lsp_button.regenerate_items(cx);
1148 let menu = ContextMenu::build(window, cx, |menu, _, cx| {
1149 state.update(cx, |state, cx| state.fill_menu(menu, cx))
1150 });
1151 lsp_button.lsp_menu = Some(menu.clone());
1152 lsp_button.popover_menu_handle.refresh_menu(
1153 window,
1154 cx,
1155 Rc::new(move |_, _| Some(menu.clone())),
1156 );
1157 cx.notify();
1158 })
1159 .ok();
1160 });
1161 }
1162 }
1163}
1164
1165impl StatusItemView for LspButton {
1166 fn set_active_pane_item(
1167 &mut self,
1168 active_pane_item: Option<&dyn workspace::ItemHandle>,
1169 window: &mut Window,
1170 cx: &mut Context<Self>,
1171 ) {
1172 if ProjectSettings::get_global(cx).global_lsp_settings.button {
1173 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
1174 if Some(&editor)
1175 != self
1176 .server_state
1177 .read(cx)
1178 .active_editor
1179 .as_ref()
1180 .and_then(|active_editor| active_editor.editor.upgrade())
1181 .as_ref()
1182 {
1183 let editor_buffers = HashSet::from_iter(
1184 editor
1185 .read(cx)
1186 .buffer()
1187 .read(cx)
1188 .snapshot(cx)
1189 .excerpts()
1190 .map(|excerpt| excerpt.context.start.buffer_id),
1191 );
1192 let _editor_subscription = cx.subscribe_in(
1193 &editor,
1194 window,
1195 |lsp_button, _, e: &EditorEvent, window, cx| match e {
1196 EditorEvent::BufferRangesUpdated { buffer, .. } => {
1197 let updated = lsp_button.server_state.update(cx, |state, cx| {
1198 if let Some(active_editor) = state.active_editor.as_mut() {
1199 let buffer_id = buffer.read(cx).remote_id();
1200 active_editor.editor_buffers.insert(buffer_id)
1201 } else {
1202 false
1203 }
1204 });
1205 if updated {
1206 lsp_button.refresh_lsp_menu(false, window, cx);
1207 }
1208 }
1209 EditorEvent::BuffersRemoved { removed_buffer_ids } => {
1210 let removed = lsp_button.server_state.update(cx, |state, _| {
1211 let mut removed = false;
1212 if let Some(active_editor) = state.active_editor.as_mut() {
1213 for id in removed_buffer_ids {
1214 active_editor.editor_buffers.retain(|buffer_id| {
1215 let retain = buffer_id != id;
1216 removed |= !retain;
1217 retain
1218 });
1219 }
1220 }
1221 removed
1222 });
1223 if removed {
1224 lsp_button.refresh_lsp_menu(false, window, cx);
1225 }
1226 }
1227 _ => {}
1228 },
1229 );
1230 self.server_state.update(cx, |state, _| {
1231 state.active_editor = Some(ActiveEditor {
1232 editor: editor.downgrade(),
1233 _editor_subscription,
1234 editor_buffers,
1235 });
1236 });
1237 self.refresh_lsp_menu(true, 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 } else if self.server_state.read(cx).active_editor.is_some() {
1246 self.server_state.update(cx, |state, _| {
1247 state.active_editor = None;
1248 });
1249 self.refresh_lsp_menu(false, window, cx);
1250 }
1251 }
1252}
1253
1254impl Render for LspButton {
1255 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
1256 if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
1257 return div().hidden();
1258 }
1259
1260 let state = self.server_state.read(cx);
1261 let is_via_ssh = state
1262 .workspace
1263 .upgrade()
1264 .map(|workspace| workspace.read(cx).project().read(cx).is_via_remote_server())
1265 .unwrap_or(false);
1266
1267 let mut has_errors = false;
1268 let mut has_warnings = false;
1269 let mut has_other_notifications = false;
1270 for binary_status in state.language_servers.binary_statuses.values() {
1271 has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
1272 has_other_notifications |= binary_status.message.is_some();
1273 }
1274
1275 for server in state.language_servers.health_statuses.values() {
1276 if let Some((message, health)) = &server.health {
1277 has_other_notifications |= message.is_some();
1278 match health {
1279 ServerHealth::Ok => {}
1280 ServerHealth::Warning => has_warnings = true,
1281 ServerHealth::Error => has_errors = true,
1282 }
1283 }
1284 }
1285
1286 let (indicator, description) = if has_errors {
1287 (
1288 Some(Indicator::dot().color(Color::Error)),
1289 "Server with errors",
1290 )
1291 } else if has_warnings {
1292 (
1293 Some(Indicator::dot().color(Color::Warning)),
1294 "Server with warnings",
1295 )
1296 } else if has_other_notifications {
1297 (
1298 Some(Indicator::dot().color(Color::Modified)),
1299 "Server with notifications",
1300 )
1301 } else {
1302 (None, "All Servers Operational")
1303 };
1304
1305 let lsp_button = cx.weak_entity();
1306
1307 div().child(
1308 PopoverMenu::new("lsp-tool")
1309 .on_open(Rc::new(move |_window, cx| {
1310 let copilot_enabled = all_language_settings(None, cx).edit_predictions.provider
1311 == EditPredictionProvider::Copilot;
1312 telemetry::event!(
1313 "Toolbar Menu Opened",
1314 name = "Language Servers",
1315 copilot_enabled,
1316 is_via_ssh,
1317 );
1318 }))
1319 .menu(move |_, cx| {
1320 lsp_button
1321 .read_with(cx, |lsp_button, _| lsp_button.lsp_menu.clone())
1322 .ok()
1323 .flatten()
1324 })
1325 .anchor(Corner::BottomLeft)
1326 .with_handle(self.popover_menu_handle.clone())
1327 .trigger_with_tooltip(
1328 IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
1329 .when_some(indicator, IconButton::indicator)
1330 .icon_size(IconSize::Small)
1331 .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
1332 move |_window, cx| {
1333 Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
1334 },
1335 ),
1336 )
1337 }
1338}