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