1use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
2use editor::Editor;
3use futures::StreamExt;
4use gpui::{
5 actions, anyhow,
6 elements::*,
7 platform::{CursorStyle, MouseButton},
8 AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
9};
10use language::{LanguageRegistry, LanguageServerBinaryStatus};
11use project::{LanguageServerProgress, Project};
12use smallvec::SmallVec;
13use std::{cmp::Reverse, fmt::Write, sync::Arc};
14use util::ResultExt;
15use workspace::{item::ItemHandle, StatusItemView, Workspace};
16
17actions!(lsp_status, [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: ModelHandle<Project>,
29 auto_updater: Option<ModelHandle<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
50pub fn init(cx: &mut AppContext) {
51 cx.add_action(ActivityIndicator::show_error_message);
52 cx.add_action(ActivityIndicator::dismiss_error_message);
53}
54
55impl ActivityIndicator {
56 pub fn new(
57 workspace: &mut Workspace,
58 languages: Arc<LanguageRegistry>,
59 cx: &mut ViewContext<Workspace>,
60 ) -> ViewHandle<ActivityIndicator> {
61 let project = workspace.project().clone();
62 let auto_updater = AutoUpdater::get(cx);
63 let this = cx.add_view(|cx: &mut ViewContext<Self>| {
64 let mut status_events = languages.language_server_binary_statuses();
65 cx.spawn(|this, mut cx| async move {
66 while let Some((language, event)) = status_events.next().await {
67 this.update(&mut cx, |this, cx| {
68 this.statuses.retain(|s| s.name != language.name());
69 this.statuses.push(LspStatus {
70 name: language.name(),
71 status: event,
72 });
73 cx.notify();
74 })?;
75 }
76 anyhow::Ok(())
77 })
78 .detach();
79 cx.observe(&project, |_, _, cx| cx.notify()).detach();
80 if let Some(auto_updater) = auto_updater.as_ref() {
81 cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
82 }
83 cx.observe_active_labeled_tasks(|_, cx| cx.notify())
84 .detach();
85
86 Self {
87 statuses: Default::default(),
88 project: project.clone(),
89 auto_updater,
90 }
91 });
92 cx.subscribe(&this, move |workspace, _, event, cx| match event {
93 Event::ShowError { lsp_name, error } => {
94 if let Some(buffer) = project
95 .update(cx, |project, cx| project.create_buffer(error, None, cx))
96 .log_err()
97 {
98 buffer.update(cx, |buffer, cx| {
99 buffer.edit(
100 [(0..0, format!("Language server error: {}\n\n", lsp_name))],
101 None,
102 cx,
103 );
104 });
105 workspace.add_item(
106 Box::new(
107 cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
108 ),
109 cx,
110 );
111 }
112 }
113 })
114 .detach();
115 this
116 }
117
118 fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
119 self.statuses.retain(|status| {
120 if let LanguageServerBinaryStatus::Failed { error } = &status.status {
121 cx.emit(Event::ShowError {
122 lsp_name: status.name.clone(),
123 error: error.clone(),
124 });
125 false
126 } else {
127 true
128 }
129 });
130
131 cx.notify();
132 }
133
134 fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
135 if let Some(updater) = &self.auto_updater {
136 updater.update(cx, |updater, cx| {
137 updater.dismiss_error(cx);
138 });
139 }
140 cx.notify();
141 }
142
143 fn pending_language_server_work<'a>(
144 &self,
145 cx: &'a AppContext,
146 ) -> impl Iterator<Item = PendingWork<'a>> {
147 self.project
148 .read(cx)
149 .language_server_statuses()
150 .rev()
151 .filter_map(|status| {
152 if status.pending_work.is_empty() {
153 None
154 } else {
155 let mut pending_work = status
156 .pending_work
157 .iter()
158 .map(|(token, progress)| PendingWork {
159 language_server_name: status.name.as_str(),
160 progress_token: token.as_str(),
161 progress,
162 })
163 .collect::<SmallVec<[_; 4]>>();
164 pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
165 Some(pending_work)
166 }
167 })
168 .flatten()
169 }
170
171 fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
172 // Show any language server has pending activity.
173 let mut pending_work = self.pending_language_server_work(cx);
174 if let Some(PendingWork {
175 language_server_name,
176 progress_token,
177 progress,
178 }) = pending_work.next()
179 {
180 let mut message = language_server_name.to_string();
181
182 message.push_str(": ");
183 if let Some(progress_message) = progress.message.as_ref() {
184 message.push_str(progress_message);
185 } else {
186 message.push_str(progress_token);
187 }
188
189 if let Some(percentage) = progress.percentage {
190 write!(&mut message, " ({}%)", percentage).unwrap();
191 }
192
193 let additional_work_count = pending_work.count();
194 if additional_work_count > 0 {
195 write!(&mut message, " + {} more", additional_work_count).unwrap();
196 }
197
198 return Content {
199 icon: None,
200 message,
201 on_click: None,
202 };
203 }
204
205 // Show any language server installation info.
206 let mut downloading = SmallVec::<[_; 3]>::new();
207 let mut checking_for_update = SmallVec::<[_; 3]>::new();
208 let mut failed = SmallVec::<[_; 3]>::new();
209 for status in &self.statuses {
210 let name = status.name.clone();
211 match status.status {
212 LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
213 LanguageServerBinaryStatus::Downloading => downloading.push(name),
214 LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
215 LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
216 }
217 }
218
219 if !downloading.is_empty() {
220 return Content {
221 icon: Some(DOWNLOAD_ICON),
222 message: format!(
223 "Downloading {} language server{}...",
224 downloading.join(", "),
225 if downloading.len() > 1 { "s" } else { "" }
226 ),
227 on_click: None,
228 };
229 } else if !checking_for_update.is_empty() {
230 return Content {
231 icon: Some(DOWNLOAD_ICON),
232 message: format!(
233 "Checking for updates to {} language server{}...",
234 checking_for_update.join(", "),
235 if checking_for_update.len() > 1 {
236 "s"
237 } else {
238 ""
239 }
240 ),
241 on_click: None,
242 };
243 } else if !failed.is_empty() {
244 return Content {
245 icon: Some(WARNING_ICON),
246 message: format!(
247 "Failed to download {} language server{}. Click to show error.",
248 failed.join(", "),
249 if failed.len() > 1 { "s" } else { "" }
250 ),
251 on_click: Some(Arc::new(|this, cx| {
252 this.show_error_message(&Default::default(), cx)
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(most_recent_active_task) = cx.active_labeled_tasks().last() {
294 return Content {
295 icon: None,
296 message: most_recent_active_task.to_string(),
297 on_click: None,
298 };
299 }
300
301 Default::default()
302 }
303}
304
305impl Entity for ActivityIndicator {
306 type Event = Event;
307}
308
309impl View for ActivityIndicator {
310 fn ui_name() -> &'static str {
311 "ActivityIndicator"
312 }
313
314 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
315 let Content {
316 icon,
317 message,
318 on_click,
319 } = self.content_to_render(cx);
320
321 let mut element = MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
322 let theme = &theme::current(cx).workspace.status_bar.lsp_status;
323 let style = if state.hovered() && on_click.is_some() {
324 theme.hovered.as_ref().unwrap_or(&theme.default)
325 } else {
326 &theme.default
327 };
328 Flex::row()
329 .with_children(icon.map(|path| {
330 Svg::new(path)
331 .with_color(style.icon_color)
332 .constrained()
333 .with_width(style.icon_width)
334 .contained()
335 .with_margin_right(style.icon_spacing)
336 .aligned()
337 .into_any_named("activity-icon")
338 }))
339 .with_child(
340 Text::new(message, style.message.clone())
341 .with_soft_wrap(false)
342 .aligned(),
343 )
344 .constrained()
345 .with_height(style.height)
346 .contained()
347 .with_style(style.container)
348 .aligned()
349 });
350
351 if let Some(on_click) = on_click.clone() {
352 element = element
353 .with_cursor_style(CursorStyle::PointingHand)
354 .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
355 }
356
357 element.into_any()
358 }
359}
360
361impl StatusItemView for ActivityIndicator {
362 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
363}