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