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