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