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