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