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 None,
103 cx,
104 );
105 }
106 }
107 })
108 .detach();
109 this
110 }
111
112 fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
113 self.statuses.retain(|status| {
114 if let LanguageServerBinaryStatus::Failed { error } = &status.status {
115 cx.emit(Event::ShowError {
116 lsp_name: status.name.0.clone(),
117 error: error.clone(),
118 });
119 false
120 } else {
121 true
122 }
123 });
124
125 cx.notify();
126 }
127
128 fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
129 if let Some(updater) = &self.auto_updater {
130 updater.update(cx, |updater, cx| {
131 updater.dismiss_error(cx);
132 });
133 }
134 cx.notify();
135 }
136
137 fn pending_language_server_work<'a>(
138 &self,
139 cx: &'a AppContext,
140 ) -> impl Iterator<Item = PendingWork<'a>> {
141 self.project
142 .read(cx)
143 .language_server_statuses()
144 .rev()
145 .filter_map(|status| {
146 if status.pending_work.is_empty() {
147 None
148 } else {
149 let mut pending_work = status
150 .pending_work
151 .iter()
152 .map(|(token, progress)| PendingWork {
153 language_server_name: status.name.as_str(),
154 progress_token: token.as_str(),
155 progress,
156 })
157 .collect::<SmallVec<[_; 4]>>();
158 pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
159 Some(pending_work)
160 }
161 })
162 .flatten()
163 }
164
165 fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
166 // Show any language server has pending activity.
167 let mut pending_work = self.pending_language_server_work(cx);
168 if let Some(PendingWork {
169 language_server_name,
170 progress_token,
171 progress,
172 }) = pending_work.next()
173 {
174 let mut message = language_server_name.to_string();
175
176 message.push_str(": ");
177 if let Some(progress_message) = progress.message.as_ref() {
178 message.push_str(progress_message);
179 } else {
180 message.push_str(progress_token);
181 }
182
183 if let Some(percentage) = progress.percentage {
184 write!(&mut message, " ({}%)", percentage).unwrap();
185 }
186
187 let additional_work_count = pending_work.count();
188 if additional_work_count > 0 {
189 write!(&mut message, " + {} more", additional_work_count).unwrap();
190 }
191
192 return Content {
193 icon: None,
194 message,
195 on_click: None,
196 };
197 }
198
199 // Show any language server installation info.
200 let mut downloading = SmallVec::<[_; 3]>::new();
201 let mut checking_for_update = SmallVec::<[_; 3]>::new();
202 let mut failed = SmallVec::<[_; 3]>::new();
203 for status in &self.statuses {
204 match status.status {
205 LanguageServerBinaryStatus::CheckingForUpdate => {
206 checking_for_update.push(status.name.0.as_ref())
207 }
208 LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
209 LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
210 LanguageServerBinaryStatus::None => {}
211 }
212 }
213
214 if !downloading.is_empty() {
215 return Content {
216 icon: Some(DOWNLOAD_ICON),
217 message: format!("Downloading {}...", downloading.join(", "),),
218 on_click: None,
219 };
220 }
221
222 if !checking_for_update.is_empty() {
223 return Content {
224 icon: Some(DOWNLOAD_ICON),
225 message: format!(
226 "Checking for updates to {}...",
227 checking_for_update.join(", "),
228 ),
229 on_click: None,
230 };
231 }
232
233 if !failed.is_empty() {
234 return Content {
235 icon: Some(WARNING_ICON),
236 message: format!(
237 "Failed to download {}. Click to show error.",
238 failed.join(", "),
239 ),
240 on_click: Some(Arc::new(|this, cx| {
241 this.show_error_message(&Default::default(), cx)
242 })),
243 };
244 }
245
246 // Show any formatting failure
247 if let Some(failure) = self.project.read(cx).last_formatting_failure() {
248 return Content {
249 icon: Some(WARNING_ICON),
250 message: format!("Formatting failed: {}. Click to see logs.", failure),
251 on_click: Some(Arc::new(|_, cx| {
252 cx.dispatch_action(Box::new(workspace::OpenLog));
253 })),
254 };
255 }
256
257 // Show any application auto-update info.
258 if let Some(updater) = &self.auto_updater {
259 return match &updater.read(cx).status() {
260 AutoUpdateStatus::Checking => Content {
261 icon: Some(DOWNLOAD_ICON),
262 message: "Checking for Zed updates…".to_string(),
263 on_click: None,
264 },
265 AutoUpdateStatus::Downloading => Content {
266 icon: Some(DOWNLOAD_ICON),
267 message: "Downloading Zed update…".to_string(),
268 on_click: None,
269 },
270 AutoUpdateStatus::Installing => Content {
271 icon: Some(DOWNLOAD_ICON),
272 message: "Installing Zed update…".to_string(),
273 on_click: None,
274 },
275 AutoUpdateStatus::Updated => Content {
276 icon: None,
277 message: "Click to restart and update Zed".to_string(),
278 on_click: Some(Arc::new(|_, cx| {
279 workspace::restart(&Default::default(), cx)
280 })),
281 },
282 AutoUpdateStatus::Errored => Content {
283 icon: Some(WARNING_ICON),
284 message: "Auto update failed".to_string(),
285 on_click: Some(Arc::new(|this, cx| {
286 this.dismiss_error_message(&Default::default(), cx)
287 })),
288 },
289 AutoUpdateStatus::Idle => Default::default(),
290 };
291 }
292
293 if let Some(extension_store) =
294 ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
295 {
296 if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
297 return Content {
298 icon: Some(DOWNLOAD_ICON),
299 message: format!("Updating {extension_id} extension…"),
300 on_click: None,
301 };
302 }
303 }
304
305 Default::default()
306 }
307}
308
309impl EventEmitter<Event> for ActivityIndicator {}
310
311impl Render for ActivityIndicator {
312 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
313 let content = self.content_to_render(cx);
314
315 let mut result = h_flex()
316 .id("activity-indicator")
317 .on_action(cx.listener(Self::show_error_message))
318 .on_action(cx.listener(Self::dismiss_error_message));
319
320 if let Some(on_click) = content.on_click {
321 result = result
322 .cursor(CursorStyle::PointingHand)
323 .on_click(cx.listener(move |this, _, cx| {
324 on_click(this, cx);
325 }))
326 }
327
328 result
329 .children(content.icon.map(|icon| svg().path(icon)))
330 .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
331 }
332}
333
334impl StatusItemView for ActivityIndicator {
335 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
336}