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 .ml_neg_1()
493 .gap_1()
494 .child(
495 Icon::new(IconName::Circle)
496 .color(status_color)
497 .size(IconSize::Small),
498 )
499 .child(
500 Label::new(status_label)
501 .size(LabelSize::Small)
502 .color(Color::Muted),
503 )
504 .when_some(metadata_label.as_ref(), |submenu, metadata| {
505 submenu
506 .child(
507 Icon::new(IconName::Dash)
508 .color(Color::Disabled)
509 .size(IconSize::XSmall),
510 )
511 .child(
512 Label::new(metadata)
513 .size(LabelSize::Small)
514 .color(Color::Muted),
515 )
516 })
517 .into_any_element()
518 }
519 });
520
521 submenu
522 }
523 },
524 );
525 }
526 menu
527 }
528}
529
530impl LanguageServers {
531 fn update_binary_status(
532 &mut self,
533 binary_status: BinaryStatus,
534 message: Option<&str>,
535 name: LanguageServerName,
536 ) {
537 let binary_status_message = message.map(SharedString::new);
538 if matches!(
539 binary_status,
540 BinaryStatus::Stopped | BinaryStatus::Failed { .. }
541 ) {
542 self.health_statuses.retain(|_, server| server.name != name);
543 }
544 self.binary_statuses.insert(
545 name,
546 LanguageServerBinaryStatus {
547 status: binary_status,
548 message: binary_status_message,
549 },
550 );
551 }
552
553 fn update_server_health(
554 &mut self,
555 id: LanguageServerId,
556 health: ServerHealth,
557 message: Option<&str>,
558 name: Option<LanguageServerName>,
559 ) {
560 if let Some(state) = self.health_statuses.get_mut(&id) {
561 state.health = Some((message.map(SharedString::new), health));
562 if let Some(name) = name {
563 state.name = name;
564 }
565 } else if let Some(name) = name {
566 self.health_statuses.insert(
567 id,
568 LanguageServerHealthStatus {
569 health: Some((message.map(SharedString::new), health)),
570 name,
571 },
572 );
573 }
574 }
575
576 fn is_empty(&self) -> bool {
577 self.binary_statuses.is_empty() && self.health_statuses.is_empty()
578 }
579}
580
581#[derive(Debug)]
582enum ServerData<'a> {
583 WithHealthCheck {
584 server_id: LanguageServerId,
585 health: &'a LanguageServerHealthStatus,
586 binary_status: Option<&'a LanguageServerBinaryStatus>,
587 },
588 WithBinaryStatus {
589 server_id: LanguageServerId,
590 server_name: &'a LanguageServerName,
591 binary_status: &'a LanguageServerBinaryStatus,
592 },
593}
594
595#[derive(Debug)]
596enum LspMenuItem {
597 WithHealthCheck {
598 server_id: LanguageServerId,
599 health: LanguageServerHealthStatus,
600 binary_status: Option<LanguageServerBinaryStatus>,
601 },
602 WithBinaryStatus {
603 server_id: LanguageServerId,
604 server_name: LanguageServerName,
605 binary_status: LanguageServerBinaryStatus,
606 },
607 ToggleServersButton {
608 restart: bool,
609 },
610 Header {
611 header: Option<SharedString>,
612 separator: bool,
613 },
614}
615
616impl LspMenuItem {
617 fn server_info(&self) -> Option<ServerInfo> {
618 match self {
619 Self::Header { .. } => None,
620 Self::ToggleServersButton { .. } => None,
621 Self::WithHealthCheck {
622 server_id,
623 health,
624 binary_status,
625 ..
626 } => Some(ServerInfo {
627 name: health.name.clone(),
628 id: *server_id,
629 health: health.health(),
630 binary_status: binary_status.clone(),
631 message: health.message(),
632 }),
633 Self::WithBinaryStatus {
634 server_id,
635 server_name,
636 binary_status,
637 ..
638 } => Some(ServerInfo {
639 name: server_name.clone(),
640 id: *server_id,
641 health: None,
642 binary_status: Some(binary_status.clone()),
643 message: binary_status.message.clone(),
644 }),
645 }
646 }
647}
648
649impl ServerData<'_> {
650 fn into_lsp_item(self) -> LspMenuItem {
651 match self {
652 Self::WithHealthCheck {
653 server_id,
654 health,
655 binary_status,
656 ..
657 } => LspMenuItem::WithHealthCheck {
658 server_id,
659 health: health.clone(),
660 binary_status: binary_status.cloned(),
661 },
662 Self::WithBinaryStatus {
663 server_id,
664 server_name,
665 binary_status,
666 ..
667 } => LspMenuItem::WithBinaryStatus {
668 server_id,
669 server_name: server_name.clone(),
670 binary_status: binary_status.clone(),
671 },
672 }
673 }
674}
675
676impl LspButton {
677 pub fn new(
678 workspace: &Workspace,
679 popover_menu_handle: PopoverMenuHandle<ContextMenu>,
680 window: &mut Window,
681 cx: &mut Context<Self>,
682 ) -> Self {
683 let settings_subscription =
684 cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
685 if ProjectSettings::get_global(cx).global_lsp_settings.button {
686 if lsp_button.lsp_menu.is_none() {
687 lsp_button.refresh_lsp_menu(true, window, cx);
688 }
689 } else if lsp_button.lsp_menu.take().is_some() {
690 cx.notify();
691 }
692 });
693
694 let lsp_store = workspace.project().read(cx).lsp_store();
695 let mut language_servers = LanguageServers::default();
696 for (_, status) in lsp_store.read(cx).language_server_statuses() {
697 language_servers.binary_statuses.insert(
698 status.name.clone(),
699 LanguageServerBinaryStatus {
700 status: BinaryStatus::None,
701 message: None,
702 },
703 );
704 }
705
706 let lsp_store_subscription =
707 cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
708 lsp_button.on_lsp_store_event(e, window, cx)
709 });
710
711 let server_state = cx.new(|_| LanguageServerState {
712 workspace: workspace.weak_handle(),
713 items: Vec::new(),
714 lsp_store: lsp_store.downgrade(),
715 active_editor: None,
716 language_servers,
717 });
718
719 let mut lsp_button = Self {
720 server_state,
721 popover_menu_handle,
722 lsp_menu: None,
723 lsp_menu_refresh: Task::ready(()),
724 _subscriptions: vec![settings_subscription, lsp_store_subscription],
725 };
726 if !lsp_button
727 .server_state
728 .read(cx)
729 .language_servers
730 .binary_statuses
731 .is_empty()
732 {
733 lsp_button.refresh_lsp_menu(true, window, cx);
734 }
735
736 lsp_button
737 }
738
739 fn on_lsp_store_event(
740 &mut self,
741 e: &LspStoreEvent,
742 window: &mut Window,
743 cx: &mut Context<Self>,
744 ) {
745 if self.lsp_menu.is_none() {
746 return;
747 };
748 let mut updated = false;
749
750 // TODO `LspStore` is global and reports status from all language servers, even from the other windows.
751 // Also, we do not get "LSP removed" events so LSPs are never removed.
752 match e {
753 LspStoreEvent::LanguageServerUpdate {
754 language_server_id,
755 name,
756 message: proto::update_language_server::Variant::StatusUpdate(status_update),
757 } => match &status_update.status {
758 Some(proto::status_update::Status::Binary(binary_status)) => {
759 let Some(name) = name.as_ref() else {
760 return;
761 };
762 if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
763 {
764 let binary_status = match binary_status {
765 proto::ServerBinaryStatus::None => BinaryStatus::None,
766 proto::ServerBinaryStatus::CheckingForUpdate => {
767 BinaryStatus::CheckingForUpdate
768 }
769 proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
770 proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
771 proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
772 proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
773 proto::ServerBinaryStatus::Failed => {
774 let Some(error) = status_update.message.clone() else {
775 return;
776 };
777 BinaryStatus::Failed { error }
778 }
779 };
780 self.server_state.update(cx, |state, _| {
781 state.language_servers.update_binary_status(
782 binary_status,
783 status_update.message.as_deref(),
784 name.clone(),
785 );
786 });
787 updated = true;
788 };
789 }
790 Some(proto::status_update::Status::Health(health_status)) => {
791 if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
792 let health = match health {
793 proto::ServerHealth::Ok => ServerHealth::Ok,
794 proto::ServerHealth::Warning => ServerHealth::Warning,
795 proto::ServerHealth::Error => ServerHealth::Error,
796 };
797 self.server_state.update(cx, |state, _| {
798 state.language_servers.update_server_health(
799 *language_server_id,
800 health,
801 status_update.message.as_deref(),
802 name.clone(),
803 );
804 });
805 updated = true;
806 }
807 }
808 None => {}
809 },
810 LspStoreEvent::LanguageServerUpdate {
811 language_server_id,
812 name,
813 message: proto::update_language_server::Variant::RegisteredForBuffer(update),
814 ..
815 } => {
816 self.server_state.update(cx, |state, cx| {
817 let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
818 workspace
819 .project()
820 .read(cx)
821 .find_worktree(Path::new(&update.buffer_abs_path), cx)
822 .map(|(worktree, _)| worktree.downgrade())
823 }) else {
824 return;
825 };
826 let entry = state
827 .language_servers
828 .servers_per_buffer_abs_path
829 .entry(PathBuf::from(&update.buffer_abs_path))
830 .or_insert_with(|| ServersForPath {
831 servers: HashMap::default(),
832 worktree: worktree.clone(),
833 });
834 entry.servers.insert(*language_server_id, name.clone());
835 if worktree.is_some() {
836 entry.worktree = worktree;
837 }
838 });
839 updated = true;
840 }
841 _ => {}
842 };
843
844 if updated {
845 self.refresh_lsp_menu(false, window, cx);
846 }
847 }
848
849 fn regenerate_items(&mut self, cx: &mut App) {
850 self.server_state.update(cx, |state, cx| {
851 let active_worktrees = state
852 .active_editor
853 .as_ref()
854 .into_iter()
855 .flat_map(|active_editor| {
856 active_editor
857 .editor
858 .upgrade()
859 .into_iter()
860 .flat_map(|active_editor| {
861 active_editor
862 .read(cx)
863 .buffer()
864 .read(cx)
865 .all_buffers()
866 .into_iter()
867 .filter_map(|buffer| {
868 project::File::from_dyn(buffer.read(cx).file())
869 })
870 .map(|buffer_file| buffer_file.worktree.clone())
871 })
872 })
873 .collect::<HashSet<_>>();
874
875 let mut server_ids_to_worktrees =
876 HashMap::<LanguageServerId, Entity<Worktree>>::default();
877 let mut server_names_to_worktrees = HashMap::<
878 LanguageServerName,
879 HashSet<(Entity<Worktree>, LanguageServerId)>,
880 >::default();
881 for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
882 if let Some(worktree) = servers_for_path
883 .worktree
884 .as_ref()
885 .and_then(|worktree| worktree.upgrade())
886 {
887 for (server_id, server_name) in &servers_for_path.servers {
888 server_ids_to_worktrees.insert(*server_id, worktree.clone());
889 if let Some(server_name) = server_name {
890 server_names_to_worktrees
891 .entry(server_name.clone())
892 .or_default()
893 .insert((worktree.clone(), *server_id));
894 }
895 }
896 }
897 }
898 state
899 .lsp_store
900 .update(cx, |lsp_store, cx| {
901 for (server_id, status) in lsp_store.language_server_statuses() {
902 if let Some(worktree) = status.worktree.and_then(|worktree_id| {
903 lsp_store
904 .worktree_store()
905 .read(cx)
906 .worktree_for_id(worktree_id, cx)
907 }) {
908 server_ids_to_worktrees.insert(server_id, worktree.clone());
909 server_names_to_worktrees
910 .entry(status.name.clone())
911 .or_default()
912 .insert((worktree, server_id));
913 }
914 }
915 })
916 .ok();
917
918 let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
919 let mut servers_with_health_checks = HashSet::default();
920
921 for (server_id, health) in &state.language_servers.health_statuses {
922 let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
923 let worktrees = server_names_to_worktrees.get(&health.name)?;
924 worktrees
925 .iter()
926 .find(|(worktree, _)| active_worktrees.contains(worktree))
927 .or_else(|| worktrees.iter().next())
928 .map(|(worktree, _)| worktree)
929 });
930 servers_with_health_checks.insert(&health.name);
931 let worktree_name =
932 worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name_str()));
933
934 let binary_status = state.language_servers.binary_statuses.get(&health.name);
935 let server_data = ServerData::WithHealthCheck {
936 server_id: *server_id,
937 health,
938 binary_status,
939 };
940 if let Some(worktree_name) = worktree_name {
941 servers_per_worktree
942 .entry(worktree_name.clone())
943 .or_default()
944 .push(server_data);
945 }
946 }
947
948 let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
949 let mut can_restart_all = state.language_servers.health_statuses.is_empty();
950 for (server_name, binary_status) in state
951 .language_servers
952 .binary_statuses
953 .iter()
954 .filter(|(name, _)| !servers_with_health_checks.contains(name))
955 {
956 match binary_status.status {
957 BinaryStatus::None => {
958 can_restart_all = false;
959 can_stop_all |= true;
960 }
961 BinaryStatus::CheckingForUpdate => {
962 can_restart_all = false;
963 can_stop_all = false;
964 }
965 BinaryStatus::Downloading => {
966 can_restart_all = false;
967 can_stop_all = false;
968 }
969 BinaryStatus::Starting => {
970 can_restart_all = false;
971 can_stop_all = false;
972 }
973 BinaryStatus::Stopping => {
974 can_restart_all = false;
975 can_stop_all = false;
976 }
977 BinaryStatus::Stopped => {}
978 BinaryStatus::Failed { .. } => {}
979 }
980
981 if let Some(worktrees_for_name) = server_names_to_worktrees.get(server_name)
982 && let Some((worktree, server_id)) = worktrees_for_name
983 .iter()
984 .find(|(worktree, _)| active_worktrees.contains(worktree))
985 .or_else(|| worktrees_for_name.iter().next())
986 {
987 let worktree_name = SharedString::new(worktree.read(cx).root_name_str());
988 servers_per_worktree
989 .entry(worktree_name.clone())
990 .or_default()
991 .push(ServerData::WithBinaryStatus {
992 server_name,
993 binary_status,
994 server_id: *server_id,
995 });
996 }
997 }
998
999 let mut new_lsp_items = Vec::with_capacity(servers_per_worktree.len() + 1);
1000 for (worktree_name, worktree_servers) in servers_per_worktree {
1001 if worktree_servers.is_empty() {
1002 continue;
1003 }
1004 new_lsp_items.push(LspMenuItem::Header {
1005 header: Some(worktree_name),
1006 separator: false,
1007 });
1008 new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
1009 }
1010 if !new_lsp_items.is_empty() {
1011 if can_stop_all {
1012 new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1013 new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
1014 } else if can_restart_all {
1015 new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1016 }
1017 }
1018
1019 state.items = new_lsp_items;
1020 });
1021 }
1022
1023 fn refresh_lsp_menu(
1024 &mut self,
1025 create_if_empty: bool,
1026 window: &mut Window,
1027 cx: &mut Context<Self>,
1028 ) {
1029 if create_if_empty || self.lsp_menu.is_some() {
1030 let state = self.server_state.clone();
1031 self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
1032 cx.background_executor()
1033 .timer(Duration::from_millis(30))
1034 .await;
1035 lsp_button
1036 .update_in(cx, |lsp_button, window, cx| {
1037 lsp_button.regenerate_items(cx);
1038 let menu = ContextMenu::build(window, cx, |menu, _, cx| {
1039 state.update(cx, |state, cx| state.fill_menu(menu, cx))
1040 });
1041 lsp_button.lsp_menu = Some(menu.clone());
1042 lsp_button.popover_menu_handle.refresh_menu(
1043 window,
1044 cx,
1045 Rc::new(move |_, _| Some(menu.clone())),
1046 );
1047 cx.notify();
1048 })
1049 .ok();
1050 });
1051 }
1052 }
1053}
1054
1055impl StatusItemView for LspButton {
1056 fn set_active_pane_item(
1057 &mut self,
1058 active_pane_item: Option<&dyn workspace::ItemHandle>,
1059 window: &mut Window,
1060 cx: &mut Context<Self>,
1061 ) {
1062 if ProjectSettings::get_global(cx).global_lsp_settings.button {
1063 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
1064 if Some(&editor)
1065 != self
1066 .server_state
1067 .read(cx)
1068 .active_editor
1069 .as_ref()
1070 .and_then(|active_editor| active_editor.editor.upgrade())
1071 .as_ref()
1072 {
1073 let editor_buffers =
1074 HashSet::from_iter(editor.read(cx).buffer().read(cx).excerpt_buffer_ids());
1075 let _editor_subscription = cx.subscribe_in(
1076 &editor,
1077 window,
1078 |lsp_button, _, e: &EditorEvent, window, cx| match e {
1079 EditorEvent::ExcerptsAdded { buffer, .. } => {
1080 let updated = lsp_button.server_state.update(cx, |state, cx| {
1081 if let Some(active_editor) = state.active_editor.as_mut() {
1082 let buffer_id = buffer.read(cx).remote_id();
1083 active_editor.editor_buffers.insert(buffer_id)
1084 } else {
1085 false
1086 }
1087 });
1088 if updated {
1089 lsp_button.refresh_lsp_menu(false, window, cx);
1090 }
1091 }
1092 EditorEvent::ExcerptsRemoved {
1093 removed_buffer_ids, ..
1094 } => {
1095 let removed = lsp_button.server_state.update(cx, |state, _| {
1096 let mut removed = false;
1097 if let Some(active_editor) = state.active_editor.as_mut() {
1098 for id in removed_buffer_ids {
1099 active_editor.editor_buffers.retain(|buffer_id| {
1100 let retain = buffer_id != id;
1101 removed |= !retain;
1102 retain
1103 });
1104 }
1105 }
1106 removed
1107 });
1108 if removed {
1109 lsp_button.refresh_lsp_menu(false, window, cx);
1110 }
1111 }
1112 _ => {}
1113 },
1114 );
1115 self.server_state.update(cx, |state, _| {
1116 state.active_editor = Some(ActiveEditor {
1117 editor: editor.downgrade(),
1118 _editor_subscription,
1119 editor_buffers,
1120 });
1121 });
1122 self.refresh_lsp_menu(true, window, cx);
1123 }
1124 } else if self.server_state.read(cx).active_editor.is_some() {
1125 self.server_state.update(cx, |state, _| {
1126 state.active_editor = None;
1127 });
1128 self.refresh_lsp_menu(false, window, cx);
1129 }
1130 } else if self.server_state.read(cx).active_editor.is_some() {
1131 self.server_state.update(cx, |state, _| {
1132 state.active_editor = None;
1133 });
1134 self.refresh_lsp_menu(false, window, cx);
1135 }
1136 }
1137}
1138
1139impl Render for LspButton {
1140 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
1141 if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
1142 return div().hidden();
1143 }
1144
1145 let mut has_errors = false;
1146 let mut has_warnings = false;
1147 let mut has_other_notifications = false;
1148 let state = self.server_state.read(cx);
1149 for binary_status in state.language_servers.binary_statuses.values() {
1150 has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
1151 has_other_notifications |= binary_status.message.is_some();
1152 }
1153
1154 for server in state.language_servers.health_statuses.values() {
1155 if let Some((message, health)) = &server.health {
1156 has_other_notifications |= message.is_some();
1157 match health {
1158 ServerHealth::Ok => {}
1159 ServerHealth::Warning => has_warnings = true,
1160 ServerHealth::Error => has_errors = true,
1161 }
1162 }
1163 }
1164
1165 let (indicator, description) = if has_errors {
1166 (
1167 Some(Indicator::dot().color(Color::Error)),
1168 "Server with errors",
1169 )
1170 } else if has_warnings {
1171 (
1172 Some(Indicator::dot().color(Color::Warning)),
1173 "Server with warnings",
1174 )
1175 } else if has_other_notifications {
1176 (
1177 Some(Indicator::dot().color(Color::Modified)),
1178 "Server with notifications",
1179 )
1180 } else {
1181 (None, "All Servers Operational")
1182 };
1183
1184 let lsp_button = cx.weak_entity();
1185
1186 div().child(
1187 PopoverMenu::new("lsp-tool")
1188 .menu(move |_, cx| {
1189 lsp_button
1190 .read_with(cx, |lsp_button, _| lsp_button.lsp_menu.clone())
1191 .ok()
1192 .flatten()
1193 })
1194 .anchor(Corner::BottomLeft)
1195 .with_handle(self.popover_menu_handle.clone())
1196 .trigger_with_tooltip(
1197 IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
1198 .when_some(indicator, IconButton::indicator)
1199 .icon_size(IconSize::Small)
1200 .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
1201 move |_window, cx| {
1202 Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
1203 },
1204 ),
1205 )
1206 }
1207}