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