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