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