1use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration};
2
3use client::proto;
4use collections::{HashMap, HashSet};
5use editor::{Editor, EditorEvent};
6use feature_flags::FeatureFlagAppExt as _;
7use gpui::{
8 Corner, DismissEvent, Entity, Focusable as _, MouseButton, Subscription, Task, WeakEntity,
9 actions,
10};
11use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
12use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
13use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
14use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
15use settings::{Settings as _, SettingsStore};
16use ui::{Context, Indicator, PopoverMenuHandle, Tooltip, Window, prelude::*};
17
18use workspace::{StatusItemView, Workspace};
19
20use crate::lsp_log::GlobalLogStore;
21
22actions!(
23 lsp_tool,
24 [
25 /// Toggles the language server tool menu.
26 ToggleMenu
27 ]
28);
29
30pub struct LspTool {
31 state: Entity<PickerState>,
32 popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
33 lsp_picker: Option<Entity<Picker<LspPickerDelegate>>>,
34 _subscriptions: Vec<Subscription>,
35}
36
37struct PickerState {
38 workspace: WeakEntity<Workspace>,
39 lsp_store: WeakEntity<LspStore>,
40 active_editor: Option<ActiveEditor>,
41 language_servers: LanguageServers,
42}
43
44#[derive(Debug)]
45pub struct LspPickerDelegate {
46 state: Entity<PickerState>,
47 selected_index: usize,
48 items: Vec<LspItem>,
49 other_servers_start_index: Option<usize>,
50}
51
52struct ActiveEditor {
53 editor: WeakEntity<Editor>,
54 _editor_subscription: Subscription,
55 editor_buffers: HashSet<BufferId>,
56}
57
58#[derive(Debug, Default, Clone)]
59struct LanguageServers {
60 health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
61 binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
62 servers_per_buffer_abs_path:
63 HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>,
64}
65
66#[derive(Debug, Clone)]
67struct LanguageServerHealthStatus {
68 name: LanguageServerName,
69 health: Option<(Option<SharedString>, ServerHealth)>,
70}
71
72#[derive(Debug, Clone)]
73struct LanguageServerBinaryStatus {
74 status: BinaryStatus,
75 message: Option<SharedString>,
76}
77
78#[derive(Debug)]
79struct ServerInfo {
80 name: LanguageServerName,
81 id: Option<LanguageServerId>,
82 health: Option<ServerHealth>,
83 binary_status: Option<LanguageServerBinaryStatus>,
84 message: Option<SharedString>,
85}
86
87impl ServerInfo {
88 fn server_selector(&self) -> LanguageServerSelector {
89 self.id
90 .map(LanguageServerSelector::Id)
91 .unwrap_or_else(|| LanguageServerSelector::Name(self.name.clone()))
92 }
93}
94
95impl LanguageServerHealthStatus {
96 fn health(&self) -> Option<ServerHealth> {
97 self.health.as_ref().map(|(_, health)| *health)
98 }
99
100 fn message(&self) -> Option<SharedString> {
101 self.health
102 .as_ref()
103 .and_then(|(message, _)| message.clone())
104 }
105}
106
107impl LspPickerDelegate {
108 fn regenerate_items(&mut self, cx: &mut Context<Picker<Self>>) {
109 self.state.update(cx, |state, cx| {
110 let editor_buffers = state
111 .active_editor
112 .as_ref()
113 .map(|active_editor| active_editor.editor_buffers.clone())
114 .unwrap_or_default();
115 let editor_buffer_paths = editor_buffers
116 .iter()
117 .filter_map(|buffer_id| {
118 let buffer_path = state
119 .lsp_store
120 .update(cx, |lsp_store, cx| {
121 Some(
122 project::File::from_dyn(
123 lsp_store
124 .buffer_store()
125 .read(cx)
126 .get(*buffer_id)?
127 .read(cx)
128 .file(),
129 )?
130 .abs_path(cx),
131 )
132 })
133 .ok()??;
134 Some(buffer_path)
135 })
136 .collect::<Vec<_>>();
137
138 let mut servers_with_health_checks = HashSet::default();
139 let mut server_ids_with_health_checks = HashSet::default();
140 let mut buffer_servers =
141 Vec::with_capacity(state.language_servers.health_statuses.len());
142 let mut other_servers =
143 Vec::with_capacity(state.language_servers.health_statuses.len());
144 let buffer_server_ids = editor_buffer_paths
145 .iter()
146 .filter_map(|buffer_path| {
147 state
148 .language_servers
149 .servers_per_buffer_abs_path
150 .get(buffer_path)
151 })
152 .flatten()
153 .fold(HashMap::default(), |mut acc, (server_id, name)| {
154 match acc.entry(*server_id) {
155 hash_map::Entry::Occupied(mut o) => {
156 let old_name: &mut Option<&LanguageServerName> = o.get_mut();
157 if old_name.is_none() {
158 *old_name = name.as_ref();
159 }
160 }
161 hash_map::Entry::Vacant(v) => {
162 v.insert(name.as_ref());
163 }
164 }
165 acc
166 });
167 for (server_id, server_state) in &state.language_servers.health_statuses {
168 let binary_status = state
169 .language_servers
170 .binary_statuses
171 .get(&server_state.name);
172 servers_with_health_checks.insert(&server_state.name);
173 server_ids_with_health_checks.insert(*server_id);
174 if buffer_server_ids.contains_key(server_id) {
175 buffer_servers.push(ServerData::WithHealthCheck(
176 *server_id,
177 server_state,
178 binary_status,
179 ));
180 } else {
181 other_servers.push(ServerData::WithHealthCheck(
182 *server_id,
183 server_state,
184 binary_status,
185 ));
186 }
187 }
188
189 let mut can_stop_all = false;
190 let mut can_restart_all = true;
191
192 for (server_name, status) in state
193 .language_servers
194 .binary_statuses
195 .iter()
196 .filter(|(name, _)| !servers_with_health_checks.contains(name))
197 {
198 match status.status {
199 BinaryStatus::None => {
200 can_restart_all = false;
201 can_stop_all = true;
202 }
203 BinaryStatus::CheckingForUpdate => {
204 can_restart_all = false;
205 }
206 BinaryStatus::Downloading => {
207 can_restart_all = false;
208 }
209 BinaryStatus::Starting => {
210 can_restart_all = false;
211 }
212 BinaryStatus::Stopping => {
213 can_restart_all = false;
214 }
215 BinaryStatus::Stopped => {}
216 BinaryStatus::Failed { .. } => {}
217 }
218
219 let matching_server_id = state
220 .language_servers
221 .servers_per_buffer_abs_path
222 .iter()
223 .filter(|(path, _)| editor_buffer_paths.contains(path))
224 .flat_map(|(_, server_associations)| server_associations.iter())
225 .find_map(|(id, name)| {
226 if name.as_ref() == Some(server_name) {
227 Some(*id)
228 } else {
229 None
230 }
231 });
232 if let Some(server_id) = matching_server_id {
233 buffer_servers.push(ServerData::WithBinaryStatus(
234 Some(server_id),
235 server_name,
236 status,
237 ));
238 } else {
239 other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
240 }
241 }
242
243 buffer_servers.sort_by_key(|data| data.name().clone());
244 other_servers.sort_by_key(|data| data.name().clone());
245
246 let mut other_servers_start_index = None;
247 let mut new_lsp_items =
248 Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
249 new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
250 if !new_lsp_items.is_empty() {
251 other_servers_start_index = Some(new_lsp_items.len());
252 }
253 new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
254 if !new_lsp_items.is_empty() {
255 if can_stop_all {
256 new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
257 } else if can_restart_all {
258 new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
259 }
260 }
261
262 self.items = new_lsp_items;
263 self.other_servers_start_index = other_servers_start_index;
264 });
265 }
266
267 fn server_info(&self, ix: usize) -> Option<ServerInfo> {
268 match self.items.get(ix)? {
269 LspItem::ToggleServersButton { .. } => None,
270 LspItem::WithHealthCheck(
271 language_server_id,
272 language_server_health_status,
273 language_server_binary_status,
274 ) => Some(ServerInfo {
275 name: language_server_health_status.name.clone(),
276 id: Some(*language_server_id),
277 health: language_server_health_status.health(),
278 binary_status: language_server_binary_status.clone(),
279 message: language_server_health_status.message(),
280 }),
281 LspItem::WithBinaryStatus(
282 server_id,
283 language_server_name,
284 language_server_binary_status,
285 ) => Some(ServerInfo {
286 name: language_server_name.clone(),
287 id: *server_id,
288 health: None,
289 binary_status: Some(language_server_binary_status.clone()),
290 message: language_server_binary_status.message.clone(),
291 }),
292 }
293 }
294}
295
296impl LanguageServers {
297 fn update_binary_status(
298 &mut self,
299 binary_status: BinaryStatus,
300 message: Option<&str>,
301 name: LanguageServerName,
302 ) {
303 let binary_status_message = message.map(SharedString::new);
304 if matches!(
305 binary_status,
306 BinaryStatus::Stopped | BinaryStatus::Failed { .. }
307 ) {
308 self.health_statuses.retain(|_, server| server.name != name);
309 }
310 self.binary_statuses.insert(
311 name,
312 LanguageServerBinaryStatus {
313 status: binary_status,
314 message: binary_status_message,
315 },
316 );
317 }
318
319 fn update_server_health(
320 &mut self,
321 id: LanguageServerId,
322 health: ServerHealth,
323 message: Option<&str>,
324 name: Option<LanguageServerName>,
325 ) {
326 if let Some(state) = self.health_statuses.get_mut(&id) {
327 state.health = Some((message.map(SharedString::new), health));
328 if let Some(name) = name {
329 state.name = name;
330 }
331 } else if let Some(name) = name {
332 self.health_statuses.insert(
333 id,
334 LanguageServerHealthStatus {
335 health: Some((message.map(SharedString::new), health)),
336 name,
337 },
338 );
339 }
340 }
341
342 fn is_empty(&self) -> bool {
343 self.binary_statuses.is_empty() && self.health_statuses.is_empty()
344 }
345}
346
347#[derive(Debug)]
348enum ServerData<'a> {
349 WithHealthCheck(
350 LanguageServerId,
351 &'a LanguageServerHealthStatus,
352 Option<&'a LanguageServerBinaryStatus>,
353 ),
354 WithBinaryStatus(
355 Option<LanguageServerId>,
356 &'a LanguageServerName,
357 &'a LanguageServerBinaryStatus,
358 ),
359}
360
361#[derive(Debug)]
362enum LspItem {
363 WithHealthCheck(
364 LanguageServerId,
365 LanguageServerHealthStatus,
366 Option<LanguageServerBinaryStatus>,
367 ),
368 WithBinaryStatus(
369 Option<LanguageServerId>,
370 LanguageServerName,
371 LanguageServerBinaryStatus,
372 ),
373 ToggleServersButton {
374 restart: bool,
375 },
376}
377
378impl ServerData<'_> {
379 fn name(&self) -> &LanguageServerName {
380 match self {
381 Self::WithHealthCheck(_, state, _) => &state.name,
382 Self::WithBinaryStatus(_, name, ..) => name,
383 }
384 }
385
386 fn into_lsp_item(self) -> LspItem {
387 match self {
388 Self::WithHealthCheck(id, name, status) => {
389 LspItem::WithHealthCheck(id, name.clone(), status.cloned())
390 }
391 Self::WithBinaryStatus(server_id, name, status) => {
392 LspItem::WithBinaryStatus(server_id, name.clone(), status.clone())
393 }
394 }
395 }
396}
397
398impl PickerDelegate for LspPickerDelegate {
399 type ListItem = AnyElement;
400
401 fn match_count(&self) -> usize {
402 self.items.len()
403 }
404
405 fn selected_index(&self) -> usize {
406 self.selected_index
407 }
408
409 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
410 self.selected_index = ix;
411 cx.notify();
412 }
413
414 fn update_matches(
415 &mut self,
416 _: String,
417 _: &mut Window,
418 cx: &mut Context<Picker<Self>>,
419 ) -> Task<()> {
420 cx.spawn(async move |lsp_picker, cx| {
421 cx.background_executor()
422 .timer(Duration::from_millis(30))
423 .await;
424 lsp_picker
425 .update(cx, |lsp_picker, cx| {
426 lsp_picker.delegate.regenerate_items(cx);
427 })
428 .ok();
429 })
430 }
431
432 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
433 Arc::default()
434 }
435
436 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
437 if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(self.selected_index)
438 {
439 let lsp_store = self.state.read(cx).lsp_store.clone();
440 lsp_store
441 .update(cx, |lsp_store, cx| {
442 if *restart {
443 let Some(workspace) = self.state.read(cx).workspace.upgrade() else {
444 return;
445 };
446 let project = workspace.read(cx).project().clone();
447 let buffer_store = project.read(cx).buffer_store().clone();
448 let worktree_store = project.read(cx).worktree_store();
449
450 let buffers = self
451 .state
452 .read(cx)
453 .language_servers
454 .servers_per_buffer_abs_path
455 .keys()
456 .filter_map(|abs_path| {
457 worktree_store.read(cx).find_worktree(abs_path, cx)
458 })
459 .filter_map(|(worktree, relative_path)| {
460 let entry = worktree.read(cx).entry_for_path(&relative_path)?;
461 project.read(cx).path_for_entry(entry.id, cx)
462 })
463 .filter_map(|project_path| {
464 buffer_store.read(cx).get_by_path(&project_path)
465 })
466 .collect();
467 let selectors = self
468 .items
469 .iter()
470 // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
471 .flat_map(|item| match item {
472 LspItem::ToggleServersButton { .. } => None,
473 LspItem::WithHealthCheck(_, status, ..) => {
474 Some(LanguageServerSelector::Name(status.name.clone()))
475 }
476 LspItem::WithBinaryStatus(_, server_name, ..) => {
477 Some(LanguageServerSelector::Name(server_name.clone()))
478 }
479 })
480 .collect();
481 lsp_store.restart_language_servers_for_buffers(buffers, selectors, cx);
482 } else {
483 lsp_store.stop_all_language_servers(cx);
484 }
485 })
486 .ok();
487 }
488
489 let Some(server_selector) = self
490 .server_info(self.selected_index)
491 .map(|info| info.server_selector())
492 else {
493 return;
494 };
495 let lsp_logs = cx.global::<GlobalLogStore>().0.clone();
496 let lsp_store = self.state.read(cx).lsp_store.clone();
497 let workspace = self.state.read(cx).workspace.clone();
498 lsp_logs
499 .update(cx, |lsp_logs, cx| {
500 let has_logs = lsp_store
501 .update(cx, |lsp_store, _| {
502 lsp_store.as_local().is_some() && lsp_logs.has_server_logs(&server_selector)
503 })
504 .unwrap_or(false);
505 if has_logs {
506 lsp_logs.open_server_trace(workspace, server_selector, window, cx);
507 }
508 })
509 .ok();
510 }
511
512 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
513 cx.emit(DismissEvent);
514 }
515
516 fn render_match(
517 &self,
518 ix: usize,
519 selected: bool,
520 _: &mut Window,
521 cx: &mut Context<Picker<Self>>,
522 ) -> Option<Self::ListItem> {
523 let rendered_match = h_flex().px_1().gap_1();
524 let rendered_match_contents = h_flex()
525 .id(("lsp-item", ix))
526 .w_full()
527 .px_2()
528 .gap_2()
529 .when(selected, |server_entry| {
530 server_entry.bg(cx.theme().colors().element_hover)
531 })
532 .hover(|s| s.bg(cx.theme().colors().element_hover));
533
534 if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(ix) {
535 let label = Label::new(if *restart {
536 "Restart All Servers"
537 } else {
538 "Stop All Servers"
539 });
540 return Some(
541 rendered_match
542 .child(rendered_match_contents.child(label))
543 .into_any_element(),
544 );
545 }
546
547 let server_info = self.server_info(ix)?;
548 let workspace = self.state.read(cx).workspace.clone();
549 let lsp_logs = cx.global::<GlobalLogStore>().0.upgrade()?;
550 let lsp_store = self.state.read(cx).lsp_store.upgrade()?;
551 let server_selector = server_info.server_selector();
552
553 // TODO currently, Zed remote does not work well with the LSP logs
554 // https://github.com/zed-industries/zed/issues/28557
555 let has_logs = lsp_store.read(cx).as_local().is_some()
556 && lsp_logs.read(cx).has_server_logs(&server_selector);
557
558 let status_color = server_info
559 .binary_status
560 .and_then(|binary_status| match binary_status.status {
561 BinaryStatus::None => None,
562 BinaryStatus::CheckingForUpdate
563 | BinaryStatus::Downloading
564 | BinaryStatus::Starting => Some(Color::Modified),
565 BinaryStatus::Stopping => Some(Color::Disabled),
566 BinaryStatus::Stopped => Some(Color::Disabled),
567 BinaryStatus::Failed { .. } => Some(Color::Error),
568 })
569 .or_else(|| {
570 Some(match server_info.health? {
571 ServerHealth::Ok => Color::Success,
572 ServerHealth::Warning => Color::Warning,
573 ServerHealth::Error => Color::Error,
574 })
575 })
576 .unwrap_or(Color::Success);
577
578 Some(
579 rendered_match
580 .child(
581 rendered_match_contents
582 .child(Indicator::dot().color(status_color))
583 .child(Label::new(server_info.name.0.clone()))
584 .when_some(
585 server_info.message.clone(),
586 |server_entry, server_message| {
587 server_entry.tooltip(Tooltip::text(server_message.clone()))
588 },
589 ),
590 )
591 .when_else(
592 has_logs,
593 |server_entry| {
594 server_entry.on_mouse_down(MouseButton::Left, {
595 let workspace = workspace.clone();
596 let lsp_logs = lsp_logs.downgrade();
597 let server_selector = server_selector.clone();
598 move |_, window, cx| {
599 lsp_logs
600 .update(cx, |lsp_logs, cx| {
601 lsp_logs.open_server_trace(
602 workspace.clone(),
603 server_selector.clone(),
604 window,
605 cx,
606 );
607 })
608 .ok();
609 }
610 })
611 },
612 |div| div.cursor_default(),
613 )
614 .into_any_element(),
615 )
616 }
617
618 fn render_editor(
619 &self,
620 editor: &Entity<Editor>,
621 _: &mut Window,
622 cx: &mut Context<Picker<Self>>,
623 ) -> Div {
624 div().child(div().track_focus(&editor.focus_handle(cx)))
625 }
626
627 fn separators_after_indices(&self) -> Vec<usize> {
628 if self.items.is_empty() {
629 return Vec::new();
630 }
631 let mut indices = vec![self.items.len().saturating_sub(2)];
632 if let Some(other_servers_start_index) = self.other_servers_start_index {
633 if other_servers_start_index > 0 {
634 indices.insert(0, other_servers_start_index - 1);
635 indices.dedup();
636 }
637 }
638 indices
639 }
640}
641
642impl LspTool {
643 pub fn new(
644 workspace: &Workspace,
645 popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
646 window: &mut Window,
647 cx: &mut Context<Self>,
648 ) -> Self {
649 let settings_subscription =
650 cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
651 if ProjectSettings::get_global(cx).global_lsp_settings.button {
652 if lsp_tool.lsp_picker.is_none() {
653 lsp_tool.lsp_picker =
654 Some(Self::new_lsp_picker(lsp_tool.state.clone(), window, cx));
655 cx.notify();
656 return;
657 }
658 } else if lsp_tool.lsp_picker.take().is_some() {
659 cx.notify();
660 }
661 });
662
663 let lsp_store = workspace.project().read(cx).lsp_store();
664 let lsp_store_subscription =
665 cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| {
666 lsp_tool.on_lsp_store_event(e, window, cx)
667 });
668
669 let state = cx.new(|_| PickerState {
670 workspace: workspace.weak_handle(),
671 lsp_store: lsp_store.downgrade(),
672 active_editor: None,
673 language_servers: LanguageServers::default(),
674 });
675
676 Self {
677 state,
678 popover_menu_handle,
679 lsp_picker: None,
680 _subscriptions: vec![settings_subscription, lsp_store_subscription],
681 }
682 }
683
684 fn on_lsp_store_event(
685 &mut self,
686 e: &LspStoreEvent,
687 window: &mut Window,
688 cx: &mut Context<Self>,
689 ) {
690 let Some(lsp_picker) = self.lsp_picker.clone() else {
691 return;
692 };
693 let mut updated = false;
694
695 match e {
696 LspStoreEvent::LanguageServerUpdate {
697 language_server_id,
698 name,
699 message: proto::update_language_server::Variant::StatusUpdate(status_update),
700 } => match &status_update.status {
701 Some(proto::status_update::Status::Binary(binary_status)) => {
702 let Some(name) = name.as_ref() else {
703 return;
704 };
705 if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
706 {
707 let binary_status = match binary_status {
708 proto::ServerBinaryStatus::None => BinaryStatus::None,
709 proto::ServerBinaryStatus::CheckingForUpdate => {
710 BinaryStatus::CheckingForUpdate
711 }
712 proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
713 proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
714 proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
715 proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
716 proto::ServerBinaryStatus::Failed => {
717 let Some(error) = status_update.message.clone() else {
718 return;
719 };
720 BinaryStatus::Failed { error }
721 }
722 };
723 self.state.update(cx, |state, _| {
724 state.language_servers.update_binary_status(
725 binary_status,
726 status_update.message.as_deref(),
727 name.clone(),
728 );
729 });
730 updated = true;
731 };
732 }
733 Some(proto::status_update::Status::Health(health_status)) => {
734 if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
735 let health = match health {
736 proto::ServerHealth::Ok => ServerHealth::Ok,
737 proto::ServerHealth::Warning => ServerHealth::Warning,
738 proto::ServerHealth::Error => ServerHealth::Error,
739 };
740 self.state.update(cx, |state, _| {
741 state.language_servers.update_server_health(
742 *language_server_id,
743 health,
744 status_update.message.as_deref(),
745 name.clone(),
746 );
747 });
748 updated = true;
749 }
750 }
751 None => {}
752 },
753 LspStoreEvent::LanguageServerUpdate {
754 language_server_id,
755 name,
756 message: proto::update_language_server::Variant::RegisteredForBuffer(update),
757 ..
758 } => {
759 self.state.update(cx, |state, _| {
760 state
761 .language_servers
762 .servers_per_buffer_abs_path
763 .entry(PathBuf::from(&update.buffer_abs_path))
764 .or_default()
765 .insert(*language_server_id, name.clone());
766 });
767 updated = true;
768 }
769 _ => {}
770 };
771
772 if updated {
773 lsp_picker.update(cx, |lsp_picker, cx| {
774 lsp_picker.refresh(window, cx);
775 });
776 }
777 }
778
779 fn new_lsp_picker(
780 state: Entity<PickerState>,
781 window: &mut Window,
782 cx: &mut Context<Self>,
783 ) -> Entity<Picker<LspPickerDelegate>> {
784 cx.new(|cx| {
785 let mut delegate = LspPickerDelegate {
786 selected_index: 0,
787 other_servers_start_index: None,
788 items: Vec::new(),
789 state,
790 };
791 delegate.regenerate_items(cx);
792 Picker::list(delegate, window, cx)
793 })
794 }
795}
796
797impl StatusItemView for LspTool {
798 fn set_active_pane_item(
799 &mut self,
800 active_pane_item: Option<&dyn workspace::ItemHandle>,
801 window: &mut Window,
802 cx: &mut Context<Self>,
803 ) {
804 if ProjectSettings::get_global(cx).global_lsp_settings.button {
805 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
806 if Some(&editor)
807 != self
808 .state
809 .read(cx)
810 .active_editor
811 .as_ref()
812 .and_then(|active_editor| active_editor.editor.upgrade())
813 .as_ref()
814 {
815 let editor_buffers =
816 HashSet::from_iter(editor.read(cx).buffer().read(cx).excerpt_buffer_ids());
817 let _editor_subscription = cx.subscribe_in(
818 &editor,
819 window,
820 |lsp_tool, _, e: &EditorEvent, window, cx| match e {
821 EditorEvent::ExcerptsAdded { buffer, .. } => {
822 lsp_tool.state.update(cx, |state, cx| {
823 if let Some(active_editor) = state.active_editor.as_mut() {
824 let buffer_id = buffer.read(cx).remote_id();
825 if active_editor.editor_buffers.insert(buffer_id) {
826 if let Some(picker) = &lsp_tool.lsp_picker {
827 picker.update(cx, |picker, cx| {
828 picker.refresh(window, cx)
829 });
830 }
831 }
832 }
833 });
834 }
835 EditorEvent::ExcerptsRemoved {
836 removed_buffer_ids, ..
837 } => {
838 lsp_tool.state.update(cx, |state, cx| {
839 if let Some(active_editor) = state.active_editor.as_mut() {
840 let mut removed = false;
841 for id in removed_buffer_ids {
842 active_editor.editor_buffers.retain(|buffer_id| {
843 let retain = buffer_id != id;
844 removed |= !retain;
845 retain
846 });
847 }
848 if removed {
849 if let Some(picker) = &lsp_tool.lsp_picker {
850 picker.update(cx, |picker, cx| {
851 picker.refresh(window, cx)
852 });
853 }
854 }
855 }
856 });
857 }
858 _ => {}
859 },
860 );
861 self.state.update(cx, |state, _| {
862 state.active_editor = Some(ActiveEditor {
863 editor: editor.downgrade(),
864 _editor_subscription,
865 editor_buffers,
866 });
867 });
868
869 let lsp_picker = Self::new_lsp_picker(self.state.clone(), window, cx);
870 self.lsp_picker = Some(lsp_picker.clone());
871 lsp_picker.update(cx, |lsp_picker, cx| lsp_picker.refresh(window, cx));
872 }
873 } else if self.state.read(cx).active_editor.is_some() {
874 self.state.update(cx, |state, _| {
875 state.active_editor = None;
876 });
877 if let Some(lsp_picker) = self.lsp_picker.as_ref() {
878 lsp_picker.update(cx, |lsp_picker, cx| {
879 lsp_picker.refresh(window, cx);
880 });
881 };
882 }
883 } else if self.state.read(cx).active_editor.is_some() {
884 self.state.update(cx, |state, _| {
885 state.active_editor = None;
886 });
887 if let Some(lsp_picker) = self.lsp_picker.as_ref() {
888 lsp_picker.update(cx, |lsp_picker, cx| {
889 lsp_picker.refresh(window, cx);
890 });
891 }
892 }
893 }
894}
895
896impl Render for LspTool {
897 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
898 if !cx.is_staff() || self.state.read(cx).language_servers.is_empty() {
899 return div();
900 }
901
902 let Some(lsp_picker) = self.lsp_picker.clone() else {
903 return div();
904 };
905
906 let mut has_errors = false;
907 let mut has_warnings = false;
908 let mut has_other_notifications = false;
909 let state = self.state.read(cx);
910 for server in state.language_servers.health_statuses.values() {
911 if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) {
912 has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
913 has_other_notifications |= binary_status.message.is_some();
914 }
915
916 if let Some((message, health)) = &server.health {
917 has_other_notifications |= message.is_some();
918 match health {
919 ServerHealth::Ok => {}
920 ServerHealth::Warning => has_warnings = true,
921 ServerHealth::Error => has_errors = true,
922 }
923 }
924 }
925
926 let indicator = if has_errors {
927 Some(Indicator::dot().color(Color::Error))
928 } else if has_warnings {
929 Some(Indicator::dot().color(Color::Warning))
930 } else if has_other_notifications {
931 Some(Indicator::dot().color(Color::Modified))
932 } else {
933 None
934 };
935
936 div().child(
937 PickerPopoverMenu::new(
938 lsp_picker.clone(),
939 IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt)
940 .when_some(indicator, IconButton::indicator)
941 .icon_size(IconSize::Small)
942 .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
943 move |window, cx| Tooltip::for_action("Language Servers", &ToggleMenu, window, cx),
944 Corner::BottomLeft,
945 cx,
946 )
947 .with_handle(self.popover_menu_handle.clone())
948 .render(window, cx),
949 )
950 }
951}