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