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_12.svg";
20const WARNING_ICON: &str = "icons/triangle_exclamation_12.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 match status.status {
211 LanguageServerBinaryStatus::CheckingForUpdate => {
212 checking_for_update.push(status.name.clone());
213 }
214 LanguageServerBinaryStatus::Downloading => {
215 downloading.push(status.name.clone());
216 }
217 LanguageServerBinaryStatus::Failed { .. } => {
218 failed.push(status.name.clone());
219 }
220 LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
221 }
222 }
223
224 if !downloading.is_empty() {
225 return Content {
226 icon: Some(DOWNLOAD_ICON),
227 message: format!(
228 "Downloading {} language server{}...",
229 downloading.join(", "),
230 if downloading.len() > 1 { "s" } else { "" }
231 ),
232 on_click: None,
233 };
234 } else if !checking_for_update.is_empty() {
235 return Content {
236 icon: Some(DOWNLOAD_ICON),
237 message: format!(
238 "Checking for updates to {} language server{}...",
239 checking_for_update.join(", "),
240 if checking_for_update.len() > 1 {
241 "s"
242 } else {
243 ""
244 }
245 ),
246 on_click: None,
247 };
248 } else if !failed.is_empty() {
249 return Content {
250 icon: Some(WARNING_ICON),
251 message: format!(
252 "Failed to download {} language server{}. Click to show error.",
253 failed.join(", "),
254 if failed.len() > 1 { "s" } else { "" }
255 ),
256 on_click: Some(Arc::new(|this, cx| {
257 this.show_error_message(&Default::default(), cx)
258 })),
259 };
260 }
261
262 // Show any application auto-update info.
263 if let Some(updater) = &self.auto_updater {
264 return match &updater.read(cx).status() {
265 AutoUpdateStatus::Checking => Content {
266 icon: Some(DOWNLOAD_ICON),
267 message: "Checking for Zed updates…".to_string(),
268 on_click: None,
269 },
270 AutoUpdateStatus::Downloading => Content {
271 icon: Some(DOWNLOAD_ICON),
272 message: "Downloading Zed update…".to_string(),
273 on_click: None,
274 },
275 AutoUpdateStatus::Installing => Content {
276 icon: Some(DOWNLOAD_ICON),
277 message: "Installing Zed update…".to_string(),
278 on_click: None,
279 },
280 AutoUpdateStatus::Updated => Content {
281 icon: None,
282 message: "Click to restart and update Zed".to_string(),
283 on_click: Some(Arc::new(|_, cx| {
284 workspace::restart(&Default::default(), cx)
285 })),
286 },
287 AutoUpdateStatus::Errored => Content {
288 icon: Some(WARNING_ICON),
289 message: "Auto update failed".to_string(),
290 on_click: Some(Arc::new(|this, cx| {
291 this.dismiss_error_message(&Default::default(), cx)
292 })),
293 },
294 AutoUpdateStatus::Idle => Default::default(),
295 };
296 }
297
298 if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
299 return Content {
300 icon: None,
301 message: most_recent_active_task.to_string(),
302 on_click: None,
303 };
304 }
305
306 Default::default()
307 }
308}
309
310impl Entity for ActivityIndicator {
311 type Event = Event;
312}
313
314impl View for ActivityIndicator {
315 fn ui_name() -> &'static str {
316 "ActivityIndicator"
317 }
318
319 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
320 let Content {
321 icon,
322 message,
323 on_click,
324 } = self.content_to_render(cx);
325
326 let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
327 let theme = &theme::current(cx).workspace.status_bar.lsp_status;
328 let style = if state.hovered() && on_click.is_some() {
329 theme.hover.as_ref().unwrap_or(&theme.default)
330 } else {
331 &theme.default
332 };
333 Flex::row()
334 .with_children(icon.map(|path| {
335 Svg::new(path)
336 .with_color(style.icon_color)
337 .constrained()
338 .with_width(style.icon_width)
339 .contained()
340 .with_margin_right(style.icon_spacing)
341 .aligned()
342 .into_any_named("activity-icon")
343 }))
344 .with_child(
345 Text::new(message, style.message.clone())
346 .with_soft_wrap(false)
347 .aligned(),
348 )
349 .constrained()
350 .with_height(style.height)
351 .contained()
352 .with_style(style.container)
353 .aligned()
354 });
355
356 if let Some(on_click) = on_click.clone() {
357 element = element
358 .with_cursor_style(CursorStyle::PointingHand)
359 .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
360 }
361
362 element.into_any()
363 }
364}
365
366impl StatusItemView for ActivityIndicator {
367 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
368}