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