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