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_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 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 server_message = server_message.clone();
547 let process_memory_cache = process_memory_cache.clone();
548 move |_, cx| {
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 version_label =
565 server_version.as_ref().map(|v| format!("v{}", v.as_ref()));
566
567 let separator_color =
568 cx.theme().colors().icon_disabled.opacity(0.8);
569
570 v_flex()
571 .id("metadata-container")
572 .gap_1()
573 .when_some(server_message.as_ref(), |this, _| {
574 this.w(rems_from_px(240.))
575 })
576 .child(
577 h_flex()
578 .ml_neg_1()
579 .gap_1()
580 .child(
581 Icon::new(IconName::Circle)
582 .color(status_color)
583 .size(IconSize::Small),
584 )
585 .child(
586 Label::new(status_label)
587 .size(LabelSize::Small)
588 .color(Color::Muted),
589 )
590 .when_some(version_label.as_ref(), |row, version| {
591 row.child(
592 Icon::new(IconName::Dash)
593 .color(Color::Custom(separator_color))
594 .size(IconSize::XSmall),
595 )
596 .child(
597 Label::new(version)
598 .size(LabelSize::Small)
599 .color(Color::Muted),
600 )
601 })
602 .when_some(memory_label.as_ref(), |row, memory| {
603 row.child(
604 Icon::new(IconName::Dash)
605 .color(Color::Custom(separator_color))
606 .size(IconSize::XSmall),
607 )
608 .child(
609 Label::new(memory)
610 .size(LabelSize::Small)
611 .color(Color::Muted),
612 )
613 }),
614 )
615 .when_some(server_message.clone(), |container, message| {
616 container.child(
617 Label::new(message)
618 .color(Color::Muted)
619 .size(LabelSize::Small),
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 state = self.server_state.read(cx);
1255 let is_via_ssh = state
1256 .workspace
1257 .upgrade()
1258 .map(|workspace| workspace.read(cx).project().read(cx).is_via_remote_server())
1259 .unwrap_or(false);
1260
1261 let mut has_errors = false;
1262 let mut has_warnings = false;
1263 let mut has_other_notifications = false;
1264 for binary_status in state.language_servers.binary_statuses.values() {
1265 has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
1266 has_other_notifications |= binary_status.message.is_some();
1267 }
1268
1269 for server in state.language_servers.health_statuses.values() {
1270 if let Some((message, health)) = &server.health {
1271 has_other_notifications |= message.is_some();
1272 match health {
1273 ServerHealth::Ok => {}
1274 ServerHealth::Warning => has_warnings = true,
1275 ServerHealth::Error => has_errors = true,
1276 }
1277 }
1278 }
1279
1280 let (indicator, description) = if has_errors {
1281 (
1282 Some(Indicator::dot().color(Color::Error)),
1283 "Server with errors",
1284 )
1285 } else if has_warnings {
1286 (
1287 Some(Indicator::dot().color(Color::Warning)),
1288 "Server with warnings",
1289 )
1290 } else if has_other_notifications {
1291 (
1292 Some(Indicator::dot().color(Color::Modified)),
1293 "Server with notifications",
1294 )
1295 } else {
1296 (None, "All Servers Operational")
1297 };
1298
1299 let lsp_button = cx.weak_entity();
1300
1301 div().child(
1302 PopoverMenu::new("lsp-tool")
1303 .on_open(Rc::new(move |_window, cx| {
1304 let copilot_enabled = all_language_settings(None, cx).edit_predictions.provider
1305 == EditPredictionProvider::Copilot;
1306 telemetry::event!(
1307 "Toolbar Menu Opened",
1308 name = "Language Servers",
1309 copilot_enabled,
1310 is_via_ssh,
1311 );
1312 }))
1313 .menu(move |_, cx| {
1314 lsp_button
1315 .read_with(cx, |lsp_button, _| lsp_button.lsp_menu.clone())
1316 .ok()
1317 .flatten()
1318 })
1319 .anchor(Corner::BottomLeft)
1320 .with_handle(self.popover_menu_handle.clone())
1321 .trigger_with_tooltip(
1322 IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
1323 .when_some(indicator, IconButton::indicator)
1324 .icon_size(IconSize::Small)
1325 .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
1326 move |_window, cx| {
1327 Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
1328 },
1329 ),
1330 )
1331 }
1332}