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