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
36pub fn init(cx: &mut MutableAppContext) {
37 cx.add_action(ActivityIndicator::show_error_message);
38 cx.add_action(ActivityIndicator::dismiss_error_message);
39}
40
41impl ActivityIndicator {
42 pub fn new(
43 workspace: &mut Workspace,
44 languages: Arc<LanguageRegistry>,
45 cx: &mut ViewContext<Workspace>,
46 ) -> ViewHandle<ActivityIndicator> {
47 let project = workspace.project().clone();
48 let auto_updater = AutoUpdater::get(cx);
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 if let Some(auto_updater) = auto_updater.as_ref() {
70 cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
71 }
72
73 Self {
74 statuses: Default::default(),
75 project: project.clone(),
76 auto_updater,
77 }
78 });
79 cx.subscribe(&this, move |workspace, _, event, cx| match event {
80 Event::ShowError { lsp_name, error } => {
81 if let Some(buffer) = project
82 .update(cx, |project, cx| project.create_buffer(error, None, cx))
83 .log_err()
84 {
85 buffer.update(cx, |buffer, cx| {
86 buffer.edit(
87 [(0..0, format!("Language server error: {}\n\n", lsp_name))],
88 None,
89 cx,
90 );
91 });
92 workspace.add_item(
93 Box::new(
94 cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
95 ),
96 cx,
97 );
98 }
99 }
100 })
101 .detach();
102 this
103 }
104
105 fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
106 self.statuses.retain(|status| {
107 if let LanguageServerBinaryStatus::Failed { error } = &status.status {
108 cx.emit(Event::ShowError {
109 lsp_name: status.name.clone(),
110 error: error.clone(),
111 });
112 false
113 } else {
114 true
115 }
116 });
117
118 cx.notify();
119 }
120
121 fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
122 if let Some(updater) = &self.auto_updater {
123 updater.update(cx, |updater, cx| {
124 updater.dismiss_error(cx);
125 });
126 }
127 cx.notify();
128 }
129
130 fn pending_language_server_work<'a>(
131 &self,
132 cx: &'a AppContext,
133 ) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
134 self.project
135 .read(cx)
136 .language_server_statuses()
137 .rev()
138 .filter_map(|status| {
139 if status.pending_work.is_empty() {
140 None
141 } else {
142 let mut pending_work = status
143 .pending_work
144 .iter()
145 .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
146 .collect::<SmallVec<[_; 4]>>();
147 pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
148 Some(pending_work)
149 }
150 })
151 .flatten()
152 }
153
154 fn content_to_render(
155 &mut self,
156 cx: &mut RenderContext<Self>,
157 ) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
158 // Show any language server has pending activity.
159 let mut pending_work = self.pending_language_server_work(cx);
160 if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
161 let mut message = lang_server_name.to_string();
162
163 message.push_str(": ");
164 if let Some(progress_message) = progress.message.as_ref() {
165 message.push_str(progress_message);
166 } else {
167 message.push_str(progress_token);
168 }
169
170 if let Some(percentage) = progress.percentage {
171 write!(&mut message, " ({}%)", percentage).unwrap();
172 }
173
174 let additional_work_count = pending_work.count();
175 if additional_work_count > 0 {
176 write!(&mut message, " + {} more", additional_work_count).unwrap();
177 }
178
179 return (None, message, None);
180 }
181
182 // Show any language server installation info.
183 let mut downloading = SmallVec::<[_; 3]>::new();
184 let mut checking_for_update = SmallVec::<[_; 3]>::new();
185 let mut failed = SmallVec::<[_; 3]>::new();
186 for status in &self.statuses {
187 match status.status {
188 LanguageServerBinaryStatus::CheckingForUpdate => {
189 checking_for_update.push(status.name.clone());
190 }
191 LanguageServerBinaryStatus::Downloading => {
192 downloading.push(status.name.clone());
193 }
194 LanguageServerBinaryStatus::Failed { .. } => {
195 failed.push(status.name.clone());
196 }
197 LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
198 }
199 }
200
201 if !downloading.is_empty() {
202 return (
203 Some(DOWNLOAD_ICON),
204 format!(
205 "Downloading {} language server{}...",
206 downloading.join(", "),
207 if downloading.len() > 1 { "s" } else { "" }
208 ),
209 None,
210 );
211 } else if !checking_for_update.is_empty() {
212 return (
213 Some(DOWNLOAD_ICON),
214 format!(
215 "Checking for updates to {} language server{}...",
216 checking_for_update.join(", "),
217 if checking_for_update.len() > 1 {
218 "s"
219 } else {
220 ""
221 }
222 ),
223 None,
224 );
225 } else if !failed.is_empty() {
226 return (
227 Some(WARNING_ICON),
228 format!(
229 "Failed to download {} language server{}. Click to show error.",
230 failed.join(", "),
231 if failed.len() > 1 { "s" } else { "" }
232 ),
233 Some(Box::new(ShowErrorMessage)),
234 );
235 }
236
237 // Show any application auto-update info.
238 if let Some(updater) = &self.auto_updater {
239 match &updater.read(cx).status() {
240 AutoUpdateStatus::Checking => (
241 Some(DOWNLOAD_ICON),
242 "Checking for Zed updates…".to_string(),
243 None,
244 ),
245 AutoUpdateStatus::Downloading => (
246 Some(DOWNLOAD_ICON),
247 "Downloading Zed update…".to_string(),
248 None,
249 ),
250 AutoUpdateStatus::Installing => (
251 Some(DOWNLOAD_ICON),
252 "Installing Zed update…".to_string(),
253 None,
254 ),
255 AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
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}