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