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