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