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