1use std::collections::{HashMap, HashSet};
37use std::sync::Arc;
38
39use arc_swap::ArcSwap;
40use lattice_core::BufferId;
41use lattice_mode::{
42 ElementContent, ElementId, ModelineElement, ModelineElementUpdate, ModelineKey, ModelineRole,
43 ModelineService, Zone,
44};
45use lattice_runtime::EventBus;
46
47use crate::events::{
48 LspBufferAttached, LspBufferDetached, LspProgressKind, LspProgressUpdate, LspServerHealth,
49 LspServerStatusChanged,
50};
51
52pub const LSP_ELEMENT: &str = "lsp";
55
56const LSP_ROLE: &str = "modeline.mode_item";
60
61pub fn register_lsp_modeline_element(svc: &ModelineService) {
64 svc.register(ModelineElement::new(
65 ElementId::new(LSP_ELEMENT),
66 Zone::Right,
67 5,
68 ));
69}
70
71#[derive(Debug)]
78pub struct LspProgressStore {
79 progress: ArcSwap<HashMap<(Arc<str>, String), LspProgressUpdate>>,
80 server_status: ArcSwap<HashMap<Arc<str>, LspServerStatusChanged>>,
81}
82
83pub type LspProgressStoreHandle = Arc<LspProgressStore>;
86
87impl Default for LspProgressStore {
88 fn default() -> Self {
89 Self {
90 progress: ArcSwap::from_pointee(HashMap::new()),
91 server_status: ArcSwap::from_pointee(HashMap::new()),
92 }
93 }
94}
95
96impl LspProgressStore {
97 pub fn new() -> Self {
98 Self::default()
99 }
100
101 pub fn apply_progress(&self, ev: LspProgressUpdate) {
105 self.progress.rcu(|cur| {
106 let mut next = (**cur).clone();
107 let key = (ev.server_id.clone(), ev.token.clone());
108 match ev.kind {
109 LspProgressKind::Begin => {
110 next.insert(key, ev.clone());
111 }
112 LspProgressKind::Report => {
113 if let Some(prev) = next.get(&key) {
114 let title = ev.title.clone().or_else(|| prev.title.clone());
115 let merged = LspProgressUpdate {
116 server_id: ev.server_id.clone(),
117 token: ev.token.clone(),
118 kind: ev.kind,
119 title,
120 message: ev.message.clone(),
121 percentage: ev.percentage.or(prev.percentage),
122 cancellable: ev.cancellable,
123 };
124 next.insert(key, merged);
125 } else {
126 next.insert(key, ev.clone());
127 }
128 }
129 LspProgressKind::End => {
130 next.remove(&key);
131 }
132 }
133 next
134 });
135 }
136
137 pub fn set_server_status(&self, ev: LspServerStatusChanged) {
139 self.server_status.rcu(|cur| {
140 let mut next = (**cur).clone();
141 next.insert(ev.server_id.clone(), ev.clone());
142 next
143 });
144 }
145
146 pub fn progress_snapshot(&self) -> Arc<HashMap<(Arc<str>, String), LspProgressUpdate>> {
149 self.progress.load_full()
150 }
151
152 fn content(&self) -> ElementContent {
154 lsp_content(&self.progress.load_full(), &self.server_status.load_full())
155 }
156}
157
158pub fn lsp_content(
165 progress: &HashMap<(Arc<str>, String), LspProgressUpdate>,
166 server_status: &HashMap<Arc<str>, LspServerStatusChanged>,
167) -> ElementContent {
168 let indexing = progress
169 .values()
170 .any(|u| !matches!(u.kind, LspProgressKind::End));
171 let mut any_error = false;
172 let mut any_busy = false;
173 for s in server_status.values() {
174 if matches!(s.health, LspServerHealth::Error) {
175 any_error = true;
176 }
177 if !s.quiescent {
178 any_busy = true;
179 }
180 }
181 let badge = if any_error {
182 "lsp ✗"
183 } else if indexing || any_busy {
184 "lsp ⟳"
185 } else {
186 "lsp ✓"
187 };
188
189 let mut best: Option<&LspProgressUpdate> = None;
191 for update in progress.values() {
192 if matches!(update.kind, LspProgressKind::End) {
193 continue;
194 }
195 best = Some(match best {
196 None => update,
197 Some(cur) => {
198 if update.percentage.unwrap_or(0) >= cur.percentage.unwrap_or(0) {
199 update
200 } else {
201 cur
202 }
203 }
204 });
205 }
206 let mut text = badge.to_string();
207 if let Some(p) = best {
208 let mut detail = String::new();
209 if let Some(title) = &p.title {
210 detail.push_str(title);
211 }
212 if let Some(pct) = p.percentage {
213 if !detail.is_empty() {
214 detail.push(' ');
215 }
216 detail.push_str(&format!("{pct}%"));
217 }
218 if detail.is_empty() {
219 detail.push_str(&p.token);
220 }
221 text.push(' ');
222 text.push_str(&detail);
223 }
224 ElementContent::text(text, ModelineRole::new(LSP_ROLE))
225}
226
227fn push(bus: &EventBus, buf: BufferId, content: ElementContent) {
229 bus.publish_typed(ModelineElementUpdate {
230 key: ModelineKey::Buffer(buf),
231 id: ElementId::new(LSP_ELEMENT),
232 content,
233 });
234}
235
236fn broadcast(bus: &EventBus, store: &LspProgressStore, attached: &HashSet<BufferId>) {
238 if attached.is_empty() {
239 return;
240 }
241 let content = store.content();
242 for buf in attached {
243 push(bus, *buf, content.clone());
244 }
245}
246
247pub fn spawn_modeline_forwarder(
254 bus: Arc<EventBus>,
255 store: LspProgressStoreHandle,
256 runtime: &tokio::runtime::Handle,
257) {
258 use tokio::sync::mpsc;
259 let (prog_tx, mut prog_rx) = mpsc::unbounded_channel::<LspProgressUpdate>();
260 bus.subscribe_typed(prog_tx);
261 let (status_tx, mut status_rx) = mpsc::unbounded_channel::<LspServerStatusChanged>();
262 bus.subscribe_typed(status_tx);
263 let (attach_tx, mut attach_rx) = mpsc::unbounded_channel::<LspBufferAttached>();
264 bus.subscribe_typed(attach_tx);
265 let (detach_tx, mut detach_rx) = mpsc::unbounded_channel::<LspBufferDetached>();
266 bus.subscribe_typed(detach_tx);
267
268 runtime.spawn(async move {
269 let mut attached: HashSet<BufferId> = HashSet::new();
270 loop {
271 tokio::select! {
272 ev = prog_rx.recv() => match ev {
273 Some(ev) => {
274 store.apply_progress(ev);
275 broadcast(&bus, &store, &attached);
276 }
277 None => break,
278 },
279 ev = status_rx.recv() => match ev {
280 Some(ev) => {
281 store.set_server_status(ev);
282 broadcast(&bus, &store, &attached);
283 }
284 None => break,
285 },
286 ev = attach_rx.recv() => match ev {
287 Some(ev) => {
288 let buf = BufferId(ev.id.raw() as u32);
289 attached.insert(buf);
290 push(&bus, buf, store.content());
292 }
293 None => break,
294 },
295 ev = detach_rx.recv() => match ev {
296 Some(ev) => {
297 let buf = BufferId(ev.id.raw() as u32);
298 attached.remove(&buf);
299 push(&bus, buf, ElementContent::default());
301 }
302 None => break,
303 },
304 }
305 }
306 });
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 fn prog(
314 server: &str,
315 token: &str,
316 kind: LspProgressKind,
317 title: Option<&str>,
318 pct: Option<u32>,
319 ) -> LspProgressUpdate {
320 LspProgressUpdate {
321 server_id: Arc::from(server),
322 token: token.to_string(),
323 kind,
324 title: title.map(String::from),
325 message: None,
326 percentage: pct,
327 cancellable: false,
328 }
329 }
330
331 fn status(server: &str, quiescent: bool, health: LspServerHealth) -> LspServerStatusChanged {
332 LspServerStatusChanged {
333 server_id: Arc::from(server),
334 quiescent,
335 health,
336 message: None,
337 }
338 }
339
340 #[test]
342 fn badge_ok_when_quiescent() {
343 let mut ss = HashMap::new();
344 ss.insert(
345 Arc::<str>::from("rust"),
346 status("rust", true, LspServerHealth::Ok),
347 );
348 assert_eq!(lsp_content(&HashMap::new(), &ss).plain(), "lsp ✓");
349 }
350
351 #[test]
354 fn badge_busy_with_progress_detail() {
355 let mut p = HashMap::new();
356 p.insert(
357 (Arc::<str>::from("rust"), "tok1".to_string()),
358 prog(
359 "rust",
360 "tok1",
361 LspProgressKind::Report,
362 Some("indexing"),
363 Some(40),
364 ),
365 );
366 p.insert(
367 (Arc::<str>::from("rust"), "tok2".to_string()),
368 prog(
369 "rust",
370 "tok2",
371 LspProgressKind::Begin,
372 Some("building"),
373 Some(80),
374 ),
375 );
376 assert_eq!(
378 lsp_content(&p, &HashMap::new()).plain(),
379 "lsp ⟳ building 80%"
380 );
381 }
382
383 #[test]
385 fn badge_error_takes_priority() {
386 let mut ss = HashMap::new();
387 ss.insert(
388 Arc::<str>::from("rust"),
389 status("rust", false, LspServerHealth::Error),
390 );
391 assert_eq!(lsp_content(&HashMap::new(), &ss).plain(), "lsp ✗");
392 }
393
394 #[test]
397 fn store_folds_progress_and_snapshots() {
398 let store = LspProgressStore::new();
399 store.apply_progress(prog(
400 "rust",
401 "t",
402 LspProgressKind::Begin,
403 Some("scan"),
404 None,
405 ));
406 store.apply_progress(prog("rust", "t", LspProgressKind::Report, None, Some(50)));
408 let snap = store.progress_snapshot();
409 let entry = snap
410 .get(&(Arc::<str>::from("rust"), "t".to_string()))
411 .unwrap();
412 assert_eq!(entry.title.as_deref(), Some("scan"));
413 assert_eq!(entry.percentage, Some(50));
414 assert_eq!(store.content().plain(), "lsp ⟳ scan 50%");
416 store.apply_progress(prog("rust", "t", LspProgressKind::End, None, None));
418 assert!(store.progress_snapshot().is_empty());
419 }
420
421 #[test]
424 fn register_lsp_element_is_right_zone() {
425 let svc = ModelineService::new();
426 register_lsp_modeline_element(&svc);
427 let snap = svc.snapshot();
428 let el = snap
429 .registry
430 .get(&ElementId::new(LSP_ELEMENT))
431 .expect("lsp descriptor");
432 assert_eq!(el.zone, Zone::Right);
433 assert_eq!(el.priority, 5);
434 }
435}