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(|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 action: 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 action: 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 action: 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 action: Some(Box::new(ShowErrorMessage)),
258 };
259 }
260
261 // Show any application auto-update info.
262 if let Some(updater) = &self.auto_updater {
263 return match &updater.read(cx).status() {
264 AutoUpdateStatus::Checking => Content {
265 icon: Some(DOWNLOAD_ICON),
266 message: "Checking for Zed updates…".to_string(),
267 action: None,
268 },
269 AutoUpdateStatus::Downloading => Content {
270 icon: Some(DOWNLOAD_ICON),
271 message: "Downloading Zed update…".to_string(),
272 action: None,
273 },
274 AutoUpdateStatus::Installing => Content {
275 icon: Some(DOWNLOAD_ICON),
276 message: "Installing Zed update…".to_string(),
277 action: None,
278 },
279 AutoUpdateStatus::Updated => Content {
280 icon: None,
281 message: "Click to restart and update Zed".to_string(),
282 action: Some(Box::new(workspace::Restart)),
283 },
284 AutoUpdateStatus::Errored => Content {
285 icon: Some(WARNING_ICON),
286 message: "Auto update failed".to_string(),
287 action: Some(Box::new(DismissErrorMessage)),
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 action: 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 action,
319 } = self.content_to_render(cx);
320
321 let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
322 let theme = &cx
323 .global::<Settings>()
324 .theme
325 .workspace
326 .status_bar
327 .lsp_status;
328 let style = if state.hovered() && action.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(action) = action {
357 element = element
358 .with_cursor_style(CursorStyle::PointingHand)
359 .on_click(MouseButton::Left, move |_, _, cx| {
360 cx.dispatch_any_action(action.boxed_clone())
361 });
362 }
363
364 element.into_any()
365 }
366}
367
368impl StatusItemView for ActivityIndicator {
369 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
370}