1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
2use editor::Editor;
3use futures::StreamExt;
4use gpui::{
5 actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model,
6 ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View,
7 ViewContext, VisualContext as _,
8};
9use language::{LanguageRegistry, LanguageServerBinaryStatus};
10use project::{LanguageServerProgress, Project};
11use smallvec::SmallVec;
12use std::{cmp::Reverse, fmt::Write, sync::Arc};
13use ui::h_stack;
14use util::ResultExt;
15use workspace::{item::ItemHandle, StatusItemView, Workspace};
16
17actions!(activity_indicator, [ShowErrorMessage]);
18
19const DOWNLOAD_ICON: &str = "icons/download.svg";
20const WARNING_ICON: &str = "icons/warning.svg";
21
22pub enum Event {
23 ShowError { lsp_name: Arc<str>, error: String },
24}
25
26pub struct ActivityIndicator {
27 statuses: Vec<LspStatus>,
28 project: Model<Project>,
29 auto_updater: Option<Model<AutoUpdater>>,
30}
31
32struct LspStatus {
33 name: Arc<str>,
34 status: LanguageServerBinaryStatus,
35}
36
37struct PendingWork<'a> {
38 language_server_name: &'a str,
39 progress_token: &'a str,
40 progress: &'a LanguageServerProgress,
41}
42
43#[derive(Default)]
44struct Content {
45 icon: Option<&'static str>,
46 message: String,
47 on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
48}
49
50impl ActivityIndicator {
51 pub fn new(
52 workspace: &mut Workspace,
53 languages: Arc<LanguageRegistry>,
54 cx: &mut ViewContext<Workspace>,
55 ) -> View<ActivityIndicator> {
56 let project = workspace.project().clone();
57 let auto_updater = AutoUpdater::get(cx);
58 let this = cx.build_view(|cx: &mut ViewContext<Self>| {
59 let mut status_events = languages.language_server_binary_statuses();
60 cx.spawn(|this, mut cx| async move {
61 while let Some((language, event)) = status_events.next().await {
62 this.update(&mut cx, |this, cx| {
63 this.statuses.retain(|s| s.name != language.name());
64 this.statuses.push(LspStatus {
65 name: language.name(),
66 status: event,
67 });
68 cx.notify();
69 })?;
70 }
71 anyhow::Ok(())
72 })
73 .detach();
74 cx.observe(&project, |_, _, cx| cx.notify()).detach();
75
76 if let Some(auto_updater) = auto_updater.as_ref() {
77 cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
78 }
79
80 // cx.observe_active_labeled_tasks(|_, cx| cx.notify())
81 // .detach();
82
83 Self {
84 statuses: Default::default(),
85 project: project.clone(),
86 auto_updater,
87 }
88 });
89
90 cx.subscribe(&this, move |workspace, _, event, cx| match event {
91 Event::ShowError { lsp_name, error } => {
92 if let Some(buffer) = project
93 .update(cx, |project, cx| project.create_buffer(error, None, cx))
94 .log_err()
95 {
96 buffer.update(cx, |buffer, cx| {
97 buffer.edit(
98 [(0..0, format!("Language server error: {}\n\n", lsp_name))],
99 None,
100 cx,
101 );
102 });
103 workspace.add_item(
104 Box::new(cx.build_view(|cx| {
105 Editor::for_buffer(buffer, Some(project.clone()), cx)
106 })),
107 cx,
108 );
109 }
110 }
111 })
112 .detach();
113 this
114 }
115
116 fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
117 self.statuses.retain(|status| {
118 if let LanguageServerBinaryStatus::Failed { error } = &status.status {
119 cx.emit(Event::ShowError {
120 lsp_name: status.name.clone(),
121 error: error.clone(),
122 });
123 false
124 } else {
125 true
126 }
127 });
128
129 cx.notify();
130 }
131
132 fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
133 if let Some(updater) = &self.auto_updater {
134 updater.update(cx, |updater, cx| {
135 updater.dismiss_error(cx);
136 });
137 }
138 cx.notify();
139 }
140
141 fn pending_language_server_work<'a>(
142 &self,
143 cx: &'a AppContext,
144 ) -> impl Iterator<Item = PendingWork<'a>> {
145 self.project
146 .read(cx)
147 .language_server_statuses()
148 .rev()
149 .filter_map(|status| {
150 if status.pending_work.is_empty() {
151 None
152 } else {
153 let mut pending_work = status
154 .pending_work
155 .iter()
156 .map(|(token, progress)| PendingWork {
157 language_server_name: status.name.as_str(),
158 progress_token: token.as_str(),
159 progress,
160 })
161 .collect::<SmallVec<[_; 4]>>();
162 pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
163 Some(pending_work)
164 }
165 })
166 .flatten()
167 }
168
169 fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
170 // Show any language server has pending activity.
171 let mut pending_work = self.pending_language_server_work(cx);
172 if let Some(PendingWork {
173 language_server_name,
174 progress_token,
175 progress,
176 }) = pending_work.next()
177 {
178 let mut message = language_server_name.to_string();
179
180 message.push_str(": ");
181 if let Some(progress_message) = progress.message.as_ref() {
182 message.push_str(progress_message);
183 } else {
184 message.push_str(progress_token);
185 }
186
187 if let Some(percentage) = progress.percentage {
188 write!(&mut message, " ({}%)", percentage).unwrap();
189 }
190
191 let additional_work_count = pending_work.count();
192 if additional_work_count > 0 {
193 write!(&mut message, " + {} more", additional_work_count).unwrap();
194 }
195
196 return Content {
197 icon: None,
198 message,
199 on_click: None,
200 };
201 }
202
203 // Show any language server installation info.
204 let mut downloading = SmallVec::<[_; 3]>::new();
205 let mut checking_for_update = SmallVec::<[_; 3]>::new();
206 let mut failed = SmallVec::<[_; 3]>::new();
207 for status in &self.statuses {
208 let name = status.name.clone();
209 match status.status {
210 LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
211 LanguageServerBinaryStatus::Downloading => downloading.push(name),
212 LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
213 LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
214 }
215 }
216
217 if !downloading.is_empty() {
218 return Content {
219 icon: Some(DOWNLOAD_ICON),
220 message: format!(
221 "Downloading {} language server{}...",
222 downloading.join(", "),
223 if downloading.len() > 1 { "s" } else { "" }
224 ),
225 on_click: None,
226 };
227 } else if !checking_for_update.is_empty() {
228 return Content {
229 icon: Some(DOWNLOAD_ICON),
230 message: format!(
231 "Checking for updates to {} language server{}...",
232 checking_for_update.join(", "),
233 if checking_for_update.len() > 1 {
234 "s"
235 } else {
236 ""
237 }
238 ),
239 on_click: None,
240 };
241 } else if !failed.is_empty() {
242 return Content {
243 icon: Some(WARNING_ICON),
244 message: format!(
245 "Failed to download {} language server{}. Click to show error.",
246 failed.join(", "),
247 if failed.len() > 1 { "s" } else { "" }
248 ),
249 on_click: Some(Arc::new(|this, cx| {
250 this.show_error_message(&Default::default(), cx)
251 })),
252 };
253 }
254
255 // Show any application auto-update info.
256 if let Some(updater) = &self.auto_updater {
257 return match &updater.read(cx).status() {
258 AutoUpdateStatus::Checking => Content {
259 icon: Some(DOWNLOAD_ICON),
260 message: "Checking for Zed updates…".to_string(),
261 on_click: None,
262 },
263 AutoUpdateStatus::Downloading => Content {
264 icon: Some(DOWNLOAD_ICON),
265 message: "Downloading Zed update…".to_string(),
266 on_click: None,
267 },
268 AutoUpdateStatus::Installing => Content {
269 icon: Some(DOWNLOAD_ICON),
270 message: "Installing Zed update…".to_string(),
271 on_click: None,
272 },
273 AutoUpdateStatus::Updated => Content {
274 icon: None,
275 message: "Click to restart and update Zed".to_string(),
276 on_click: Some(Arc::new(|_, cx| {
277 workspace::restart(&Default::default(), cx)
278 })),
279 },
280 AutoUpdateStatus::Errored => Content {
281 icon: Some(WARNING_ICON),
282 message: "Auto update failed".to_string(),
283 on_click: Some(Arc::new(|this, cx| {
284 this.dismiss_error_message(&Default::default(), cx)
285 })),
286 },
287 AutoUpdateStatus::Idle => Default::default(),
288 };
289 }
290
291 // todo!(show active tasks)
292 // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
293 // return Content {
294 // icon: None,
295 // message: most_recent_active_task.to_string(),
296 // on_click: None,
297 // };
298 // }
299
300 Default::default()
301 }
302}
303
304impl EventEmitter<Event> for ActivityIndicator {}
305
306impl Render for ActivityIndicator {
307 type Element = Stateful<Div>;
308
309 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
310 let content = self.content_to_render(cx);
311
312 let mut result = h_stack()
313 .id("activity-indicator")
314 .on_action(cx.listener(Self::show_error_message))
315 .on_action(cx.listener(Self::dismiss_error_message));
316
317 if let Some(on_click) = content.on_click {
318 result = result
319 .cursor(CursorStyle::PointingHand)
320 .on_click(cx.listener(move |this, _, cx| {
321 on_click(this, cx);
322 }))
323 }
324
325 result
326 .children(content.icon.map(|icon| svg().path(icon)))
327 .child(SharedString::from(content.message))
328 }
329}
330
331impl StatusItemView for ActivityIndicator {
332 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
333}