1use crate::{ItemHandle, StatusItemView};
2use futures::StreamExt;
3use gpui::{actions, AppContext, EventContext};
4use gpui::{
5 elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, RenderContext,
6 View, ViewContext,
7};
8use language::{LanguageRegistry, LanguageServerBinaryStatus};
9use project::{LanguageServerProgress, Project};
10use settings::Settings;
11use smallvec::SmallVec;
12use std::cmp::Reverse;
13use std::fmt::Write;
14use std::sync::Arc;
15
16actions!(lsp_status, [DismissErrorMessage]);
17
18pub struct LspStatus {
19 checking_for_update: Vec<String>,
20 downloading: Vec<String>,
21 failed: Vec<String>,
22 project: ModelHandle<Project>,
23}
24
25pub fn init(cx: &mut MutableAppContext) {
26 cx.add_action(LspStatus::dismiss_error_message);
27}
28
29impl LspStatus {
30 pub fn new(
31 project: &ModelHandle<Project>,
32 languages: Arc<LanguageRegistry>,
33 cx: &mut ViewContext<Self>,
34 ) -> Self {
35 let mut status_events = languages.language_server_binary_statuses();
36 cx.spawn_weak(|this, mut cx| async move {
37 while let Some((language, event)) = status_events.next().await {
38 if let Some(this) = this.upgrade(&cx) {
39 this.update(&mut cx, |this, cx| {
40 for vector in [
41 &mut this.checking_for_update,
42 &mut this.downloading,
43 &mut this.failed,
44 ] {
45 vector.retain(|name| name != language.name().as_ref());
46 }
47
48 match event {
49 LanguageServerBinaryStatus::CheckingForUpdate => {
50 this.checking_for_update.push(language.name().to_string());
51 }
52 LanguageServerBinaryStatus::Downloading => {
53 this.downloading.push(language.name().to_string());
54 }
55 LanguageServerBinaryStatus::Failed => {
56 this.failed.push(language.name().to_string());
57 }
58 LanguageServerBinaryStatus::Downloaded
59 | LanguageServerBinaryStatus::Cached => {}
60 }
61
62 cx.notify();
63 });
64 } else {
65 break;
66 }
67 }
68 })
69 .detach();
70 cx.observe(project, |_, _, cx| cx.notify()).detach();
71
72 Self {
73 checking_for_update: Default::default(),
74 downloading: Default::default(),
75 failed: Default::default(),
76 project: project.clone(),
77 }
78 }
79
80 fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
81 self.failed.clear();
82 cx.notify();
83 }
84
85 fn pending_language_server_work<'a>(
86 &self,
87 cx: &'a AppContext,
88 ) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
89 self.project
90 .read(cx)
91 .language_server_statuses()
92 .rev()
93 .filter_map(|status| {
94 if status.pending_work.is_empty() {
95 None
96 } else {
97 let mut pending_work = status
98 .pending_work
99 .iter()
100 .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
101 .collect::<SmallVec<[_; 4]>>();
102 pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
103 Some(pending_work)
104 }
105 })
106 .flatten()
107 }
108}
109
110impl Entity for LspStatus {
111 type Event = ();
112}
113
114impl View for LspStatus {
115 fn ui_name() -> &'static str {
116 "LspStatus"
117 }
118
119 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
120 let mut message;
121 let mut icon = None;
122 let mut handler = None;
123
124 let mut pending_work = self.pending_language_server_work(cx);
125 if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
126 message = lang_server_name.to_string();
127
128 message.push_str(": ");
129 if let Some(progress_message) = progress.message.as_ref() {
130 message.push_str(progress_message);
131 } else {
132 message.push_str(progress_token);
133 }
134
135 if let Some(percentage) = progress.percentage {
136 write!(&mut message, " ({}%)", percentage).unwrap();
137 }
138
139 let additional_work_count = pending_work.count();
140 if additional_work_count > 0 {
141 write!(&mut message, " + {} more", additional_work_count).unwrap();
142 }
143 } else {
144 drop(pending_work);
145
146 if !self.downloading.is_empty() {
147 icon = Some("icons/download-solid-14.svg");
148 message = format!(
149 "Downloading {} language server{}...",
150 self.downloading.join(", "),
151 if self.downloading.len() > 1 { "s" } else { "" }
152 );
153 } else if !self.checking_for_update.is_empty() {
154 icon = Some("icons/download-solid-14.svg");
155 message = format!(
156 "Checking for updates to {} language server{}...",
157 self.checking_for_update.join(", "),
158 if self.checking_for_update.len() > 1 {
159 "s"
160 } else {
161 ""
162 }
163 );
164 } else if !self.failed.is_empty() {
165 icon = Some("icons/warning-solid-14.svg");
166 message = format!(
167 "Failed to download {} language server{}. Click to dismiss.",
168 self.failed.join(", "),
169 if self.failed.len() > 1 { "s" } else { "" }
170 );
171 handler = Some(|cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
172 } else {
173 return Empty::new().boxed();
174 }
175 }
176
177 let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
178 let theme = &cx
179 .global::<Settings>()
180 .theme
181 .workspace
182 .status_bar
183 .lsp_status;
184 let style = if state.hovered && handler.is_some() {
185 theme.hover.as_ref().unwrap_or(&theme.default)
186 } else {
187 &theme.default
188 };
189 Flex::row()
190 .with_children(icon.map(|path| {
191 Svg::new(path)
192 .with_color(style.icon_color)
193 .constrained()
194 .with_width(style.icon_width)
195 .contained()
196 .with_margin_right(style.icon_spacing)
197 .aligned()
198 .named("warning-icon")
199 }))
200 .with_child(Label::new(message, style.message.clone()).aligned().boxed())
201 .constrained()
202 .with_height(style.height)
203 .contained()
204 .with_style(style.container)
205 .aligned()
206 .boxed()
207 });
208
209 if let Some(handler) = handler {
210 element = element
211 .with_cursor_style(CursorStyle::PointingHand)
212 .on_click(handler);
213 }
214
215 element.boxed()
216 }
217}
218
219impl StatusItemView for LspStatus {
220 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
221}