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