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