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