1use crate::{ItemHandle, StatusItemView};
2use futures::StreamExt;
3use gpui::AppContext;
4use gpui::{
5 action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext,
6 RenderContext, View, ViewContext,
7};
8use language::{LanguageRegistry, LanguageServerBinaryStatus};
9use project::{LanguageServerProgress, Project};
10use settings::Settings;
11use smallvec::SmallVec;
12use std::cmp::Reverse;
13use std::fmt::Write;
14use std::sync::Arc;
15
16action!(DismissErrorMessage);
17
18pub struct LspStatus {
19 checking_for_update: Vec<String>,
20 downloading: Vec<String>,
21 failed: Vec<String>,
22 project: ModelHandle<Project>,
23}
24
25pub fn init(cx: &mut MutableAppContext) {
26 cx.add_action(LspStatus::dismiss_error_message);
27}
28
29impl LspStatus {
30 pub fn new(
31 project: &ModelHandle<Project>,
32 languages: Arc<LanguageRegistry>,
33 cx: &mut ViewContext<Self>,
34 ) -> Self {
35 let mut status_events = languages.language_server_binary_statuses();
36 cx.spawn_weak(|this, mut cx| async move {
37 while let Some((language, event)) = status_events.next().await {
38 if let Some(this) = this.upgrade(&cx) {
39 this.update(&mut cx, |this, cx| {
40 for vector in [
41 &mut this.checking_for_update,
42 &mut this.downloading,
43 &mut this.failed,
44 ] {
45 vector.retain(|name| name != language.name().as_ref());
46 }
47
48 match event {
49 LanguageServerBinaryStatus::CheckingForUpdate => {
50 this.checking_for_update.push(language.name().to_string());
51 }
52 LanguageServerBinaryStatus::Downloading => {
53 this.downloading.push(language.name().to_string());
54 }
55 LanguageServerBinaryStatus::Failed => {
56 this.failed.push(language.name().to_string());
57 }
58 LanguageServerBinaryStatus::Downloaded
59 | LanguageServerBinaryStatus::Cached => {}
60 }
61
62 cx.notify();
63 });
64 } else {
65 break;
66 }
67 }
68 })
69 .detach();
70 cx.observe(project, |_, _, cx| cx.notify()).detach();
71
72 Self {
73 checking_for_update: Default::default(),
74 downloading: Default::default(),
75 failed: Default::default(),
76 project: project.clone(),
77 }
78 }
79
80 fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
81 self.failed.clear();
82 cx.notify();
83 }
84
85 fn pending_language_server_work<'a>(
86 &self,
87 cx: &'a AppContext,
88 ) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
89 self.project
90 .read(cx)
91 .language_server_statuses()
92 .rev()
93 .filter_map(|status| {
94 if status.pending_work.is_empty() {
95 None
96 } else {
97 let mut pending_work = status
98 .pending_work
99 .iter()
100 .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
101 .collect::<SmallVec<[_; 4]>>();
102 pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
103 Some(pending_work)
104 }
105 })
106 .flatten()
107 }
108}
109
110impl Entity for LspStatus {
111 type Event = ();
112}
113
114impl View for LspStatus {
115 fn ui_name() -> &'static str {
116 "LspStatus"
117 }
118
119 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
120 let theme = &cx.global::<Settings>().theme;
121
122 let mut pending_work = self.pending_language_server_work(cx);
123 if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
124 let mut message = lang_server_name.to_string();
125
126 message.push_str(": ");
127 if let Some(progress_message) = progress.message.as_ref() {
128 message.push_str(progress_message);
129 } else {
130 message.push_str(progress_token);
131 }
132
133 if let Some(percentage) = progress.percentage {
134 write!(&mut message, " ({}%)", percentage).unwrap();
135 }
136
137 let additional_work_count = pending_work.count();
138 if additional_work_count > 0 {
139 write!(&mut message, " + {} more", additional_work_count).unwrap();
140 }
141
142 Label::new(message, theme.workspace.status_bar.lsp_message.clone()).boxed()
143 } else if !self.downloading.is_empty() {
144 Label::new(
145 format!(
146 "Downloading {} language server{}...",
147 self.downloading.join(", "),
148 if self.downloading.len() > 1 { "s" } else { "" }
149 ),
150 theme.workspace.status_bar.lsp_message.clone(),
151 )
152 .boxed()
153 } else if !self.checking_for_update.is_empty() {
154 Label::new(
155 format!(
156 "Checking for updates to {} language server{}...",
157 self.checking_for_update.join(", "),
158 if self.checking_for_update.len() > 1 {
159 "s"
160 } else {
161 ""
162 }
163 ),
164 theme.workspace.status_bar.lsp_message.clone(),
165 )
166 .boxed()
167 } else if !self.failed.is_empty() {
168 drop(pending_work);
169 MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
170 let theme = &cx.global::<Settings>().theme;
171 Label::new(
172 format!(
173 "Failed to download {} language server{}. Click to dismiss.",
174 self.failed.join(", "),
175 if self.failed.len() > 1 { "s" } else { "" }
176 ),
177 theme.workspace.status_bar.lsp_message.clone(),
178 )
179 .boxed()
180 })
181 .with_cursor_style(CursorStyle::PointingHand)
182 .on_click(|cx| cx.dispatch_action(DismissErrorMessage))
183 .boxed()
184 } else {
185 Empty::new().boxed()
186 }
187 }
188}
189
190impl StatusItemView for LspStatus {
191 fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
192}