1use std::collections::{HashMap, HashSet};
57use std::sync::{Arc, Mutex};
58
59use arc_swap::ArcSwap;
60use lsp_types::{Diagnostic, DiagnosticSeverity, Uri};
61use tokio::sync::broadcast;
62
63use crate::diagnostics::DiagnosticEvent;
64use crate::logging::{LogLevel, LogSource, LspLogger};
65
66#[derive(Debug, Clone)]
68struct DiagState {
69 version: Option<i32>,
70 diagnostics: Arc<[Diagnostic]>,
71}
72
73#[derive(Debug, Clone, Default)]
80struct DiagnosticsSnapshot {
81 by_key: HashMap<(Uri, Arc<str>), DiagState>,
86 by_uri: HashMap<Uri, Arc<[Diagnostic]>>,
89}
90
91#[derive(Clone, Default)]
111pub struct DiagnosticsLayer {
112 snapshot: Arc<ArcSwap<DiagnosticsSnapshot>>,
115 write: Arc<Mutex<()>>,
119 logger: LspLogger,
120 wake: Arc<arc_swap::ArcSwapOption<tokio::sync::Notify>>,
129}
130
131impl DiagnosticsLayer {
132 pub fn new(logger: LspLogger) -> Self {
135 Self {
136 snapshot: Arc::new(ArcSwap::from(Arc::new(DiagnosticsSnapshot::default()))),
137 write: Arc::new(Mutex::new(())),
138 logger,
139 wake: Arc::default(),
140 }
141 }
142
143 pub fn set_wake(&self, wake: Arc<tokio::sync::Notify>) {
148 self.wake.store(Some(wake));
149 }
150
151 pub fn snapshot_revision(&self) -> usize {
166 Arc::as_ptr(&self.snapshot.load_full()) as *const () as usize
167 }
168
169 pub fn apply(&self, event: DiagnosticEvent) {
175 let key = (event.uri.clone(), Arc::clone(&event.server_id));
176 let _g = self.write.lock().expect("DiagnosticsLayer write lock");
177 let current = self.snapshot.load_full();
178
179 if let Some(prev) = current.by_key.get(&key)
183 && let (Some(p), Some(e)) = (prev.version, event.version)
184 && e < p
185 {
186 drop(_g);
189 self.logger.log(
196 None,
197 LogLevel::Debug,
198 LogSource::Client,
199 format!(
200 "dropping stale diagnostics for {} from {} version {} (have {})",
201 event.uri.as_str(),
202 event.server_id,
203 e,
204 p
205 ),
206 );
207 return;
208 }
209
210 let mut next = (*current).clone();
211 if event.diagnostics.is_empty() {
212 next.by_key.remove(&key);
213 } else {
214 next.by_key.insert(
215 key.clone(),
216 DiagState {
217 version: event.version,
218 diagnostics: event.diagnostics,
219 },
220 );
221 }
222 rebuild_by_uri(&mut next, &key.0);
223 self.snapshot.store(Arc::new(next));
224 drop(_g);
229 if let Some(n) = self.wake.load_full() {
230 n.notify_one();
231 }
232 }
233
234 pub fn diagnostics_for(&self, uri: &Uri) -> Vec<Diagnostic> {
238 match self.snapshot.load().by_uri.get(uri) {
239 Some(arr) => arr.to_vec(),
240 None => Vec::new(),
241 }
242 }
243
244 pub fn diagnostics_arc(&self, uri: &Uri) -> Option<Arc<[Diagnostic]>> {
249 self.snapshot.load().by_uri.get(uri).cloned()
250 }
251
252 pub fn diagnostics_on_line(&self, uri: &Uri, line: u32) -> Vec<Diagnostic> {
260 let snap = self.snapshot.load();
261 match snap.by_uri.get(uri) {
262 Some(arr) => arr
263 .iter()
264 .filter(|d| d.range.start.line <= line && line <= d.range.end.line)
265 .cloned()
266 .collect(),
267 None => Vec::new(),
268 }
269 }
270
271 pub fn line_severity(&self, uri: &Uri, line: u32) -> Option<DiagnosticSeverity> {
278 let snap = self.snapshot.load();
279 snap.by_uri
280 .get(uri)?
281 .iter()
282 .filter(|d| d.range.start.line <= line && line <= d.range.end.line)
283 .filter_map(|d| d.severity)
284 .min_by_key(severity_rank)
285 }
286
287 pub fn inline_line_summary(
304 &self,
305 uri: &Uri,
306 line: u32,
307 min_rank: u8,
308 ) -> Option<InlineDiagnosticSummary> {
309 let snap = self.snapshot.load();
310 let arr = snap.by_uri.get(uri)?;
311 let mut best: Option<(u8, &Diagnostic)> = None;
312 let mut qualifying = 0usize;
313 for d in arr.iter() {
314 if d.range.start.line > line || line > d.range.end.line {
315 continue;
316 }
317 let rank = d.severity.as_ref().map(severity_rank).unwrap_or(4);
318 if rank > min_rank {
319 continue;
320 }
321 qualifying += 1;
322 if best.is_none_or(|(br, _)| rank < br) {
325 best = Some((rank, d));
326 }
327 }
328 let (rank, d) = best?;
329 let mut text = truncate_inline_message(&d.message);
330 let extra = qualifying - 1;
331 if extra > 0 {
332 use std::fmt::Write as _;
333 let _ = write!(text, " +{extra}");
334 }
335 Some(InlineDiagnosticSummary {
336 severity_rank: rank,
337 text,
338 })
339 }
340
341 pub fn iter_uris(&self) -> Vec<Uri> {
345 let snap = self.snapshot.load();
346 let mut uris: Vec<Uri> = snap
347 .by_uri
348 .keys()
349 .cloned()
350 .collect::<HashSet<_>>()
351 .into_iter()
352 .collect();
353 uris.sort_by(|a, b| a.as_str().cmp(b.as_str()));
354 uris
355 }
356
357 pub fn snapshot(&self) -> Vec<(Uri, Vec<Diagnostic>)> {
361 let snap = self.snapshot.load();
362 let mut uris: Vec<Uri> = snap.by_uri.keys().cloned().collect();
363 uris.sort_by(|a, b| a.as_str().cmp(b.as_str()));
364 uris.into_iter()
365 .map(|uri| {
366 let diags = snap
367 .by_uri
368 .get(&uri)
369 .map(|arr| arr.to_vec())
370 .unwrap_or_default();
371 (uri, diags)
372 })
373 .collect()
374 }
375
376 pub fn count(&self) -> usize {
379 self.snapshot
380 .load()
381 .by_key
382 .values()
383 .map(|s| s.diagnostics.len())
384 .sum()
385 }
386
387 pub fn severity_counts(&self) -> SeverityCounts {
390 let snap = self.snapshot.load();
391 let mut counts = SeverityCounts::default();
392 for state in snap.by_key.values() {
393 for d in state.diagnostics.iter() {
394 match d.severity {
395 Some(DiagnosticSeverity::ERROR) => counts.errors += 1,
396 Some(DiagnosticSeverity::WARNING) => counts.warnings += 1,
397 Some(DiagnosticSeverity::INFORMATION) => counts.info += 1,
398 Some(DiagnosticSeverity::HINT) => counts.hints += 1,
399 _ => counts.unknown += 1,
400 }
401 }
402 }
403 counts
404 }
405
406 pub fn clear(&self) {
408 let _g = self.write.lock().expect("DiagnosticsLayer write lock");
409 self.snapshot
410 .store(Arc::new(DiagnosticsSnapshot::default()));
411 }
412
413 pub fn clear_uri(&self, uri: &Uri) {
417 let _g = self.write.lock().expect("DiagnosticsLayer write lock");
418 let current = self.snapshot.load_full();
419 let mut next = (*current).clone();
420 next.by_key.retain(|(u, _), _| u != uri);
421 next.by_uri.remove(uri);
422 self.snapshot.store(Arc::new(next));
423 }
424
425 pub fn clear_server(&self, server_id: &Arc<str>) {
428 let _g = self.write.lock().expect("DiagnosticsLayer write lock");
429 let current = self.snapshot.load_full();
430 let mut next = (*current).clone();
431 let affected_uris: Vec<Uri> = next
432 .by_key
433 .keys()
434 .filter(|(_, s)| Arc::ptr_eq(s, server_id) || s.as_ref() == server_id.as_ref())
435 .map(|(u, _)| u.clone())
436 .collect();
437 next.by_key
438 .retain(|(_, s), _| !Arc::ptr_eq(s, server_id) && s.as_ref() != server_id.as_ref());
439 for uri in affected_uris {
440 rebuild_by_uri(&mut next, &uri);
441 }
442 self.snapshot.store(Arc::new(next));
443 }
444}
445
446fn rebuild_by_uri(next: &mut DiagnosticsSnapshot, uri: &Uri) {
452 let mut merged: Vec<Diagnostic> = next
453 .by_key
454 .iter()
455 .filter(|((u, _), _)| u == uri)
456 .flat_map(|(_, s)| s.diagnostics.iter().cloned())
457 .collect();
458 if merged.is_empty() {
459 next.by_uri.remove(uri);
460 return;
461 }
462 merged.sort_by(|a, b| {
463 a.range
464 .start
465 .line
466 .cmp(&b.range.start.line)
467 .then(a.range.start.character.cmp(&b.range.start.character))
468 });
469 next.by_uri.insert(uri.clone(), Arc::from(merged));
470}
471
472impl std::fmt::Debug for DiagnosticsLayer {
473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474 let n = self.count();
475 f.debug_struct("DiagnosticsLayer")
476 .field("total_diagnostics", &n)
477 .finish_non_exhaustive()
478 }
479}
480
481#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
483pub struct SeverityCounts {
484 pub errors: usize,
485 pub warnings: usize,
486 pub info: usize,
487 pub hints: usize,
488 pub unknown: usize,
491}
492
493impl SeverityCounts {
494 pub fn total(&self) -> usize {
496 self.errors + self.warnings + self.info + self.hints + self.unknown
497 }
498}
499
500fn severity_rank(s: &DiagnosticSeverity) -> u8 {
502 match *s {
506 DiagnosticSeverity::ERROR => 0,
507 DiagnosticSeverity::WARNING => 1,
508 DiagnosticSeverity::INFORMATION => 2,
509 DiagnosticSeverity::HINT => 3,
510 _ => 4,
511 }
512}
513
514const INLINE_SUMMARY_MAX_CHARS: usize = 80;
518
519fn truncate_inline_message(msg: &str) -> String {
524 let first = msg.lines().next().unwrap_or("").trim();
525 if first.chars().count() <= INLINE_SUMMARY_MAX_CHARS {
526 return first.to_string();
527 }
528 let truncated: String = first.chars().take(INLINE_SUMMARY_MAX_CHARS).collect();
529 format!("{truncated}…")
530}
531
532#[derive(Debug, Clone, PartialEq, Eq)]
538pub struct InlineDiagnosticSummary {
539 pub severity_rank: u8,
544 pub text: String,
547}
548
549pub async fn pump_diagnostics(
563 layer: DiagnosticsLayer,
564 mut rx: broadcast::Receiver<DiagnosticEvent>,
565) {
566 loop {
567 match rx.recv().await {
568 Ok(event) => layer.apply(event),
569 Err(broadcast::error::RecvError::Lagged(_)) => {
570 }
572 Err(broadcast::error::RecvError::Closed) => return,
573 }
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use lsp_types::{Position, Range};
581 use std::str::FromStr;
582
583 fn diag(line: u32, severity: DiagnosticSeverity, msg: &str) -> Diagnostic {
584 Diagnostic {
585 range: Range {
586 start: Position { line, character: 0 },
587 end: Position { line, character: 5 },
588 },
589 severity: Some(severity),
590 code: None,
591 code_description: None,
592 source: None,
593 message: msg.into(),
594 related_information: None,
595 tags: None,
596 data: None,
597 }
598 }
599
600 fn ev(
601 server: &str,
602 uri: &str,
603 version: Option<i32>,
604 diags: Vec<Diagnostic>,
605 ) -> DiagnosticEvent {
606 DiagnosticEvent {
607 server_id: Arc::from(server),
608 uri: Uri::from_str(uri).unwrap(),
609 version,
610 diagnostics: Arc::from(diags.into_boxed_slice()),
611 }
612 }
613
614 fn layer() -> DiagnosticsLayer {
615 DiagnosticsLayer::new(LspLogger::with_defaults())
616 }
617
618 #[test]
619 fn apply_stores_diagnostics() {
620 let l = layer();
621 l.apply(ev(
622 "rust",
623 "file:///x.rs",
624 Some(1),
625 vec![diag(0, DiagnosticSeverity::ERROR, "boom")],
626 ));
627 let uri = Uri::from_str("file:///x.rs").unwrap();
628 let d = l.diagnostics_for(&uri);
629 assert_eq!(d.len(), 1);
630 assert_eq!(d[0].message, "boom");
631 }
632
633 #[test]
642 fn snapshot_revision_changes_on_apply_stable_otherwise() {
643 let l = layer();
644 let r0 = l.snapshot_revision();
645 assert_eq!(
647 r0,
648 l.snapshot_revision(),
649 "revision must be stable while unchanged"
650 );
651 l.apply(ev(
653 "rust",
654 "file:///x.rs",
655 Some(1),
656 vec![diag(0, DiagnosticSeverity::ERROR, "boom")],
657 ));
658 let r1 = l.snapshot_revision();
659 assert_ne!(
660 r0, r1,
661 "a diagnostics push must change the snapshot revision"
662 );
663 l.apply(ev("rust", "file:///x.rs", Some(2), vec![]));
665 assert_ne!(
666 r1,
667 l.snapshot_revision(),
668 "clearing diagnostics must change the snapshot revision"
669 );
670 }
671
672 #[tokio::test]
673 async fn apply_fires_the_render_wake() {
674 let l = layer();
675 let notify = std::sync::Arc::new(tokio::sync::Notify::new());
676 l.set_wake(notify.clone());
677 l.apply(ev(
681 "rust",
682 "file:///x.rs",
683 Some(1),
684 vec![diag(0, DiagnosticSeverity::ERROR, "boom")],
685 ));
686 tokio::time::timeout(std::time::Duration::from_millis(200), notify.notified())
687 .await
688 .expect("apply should fire the render-wake");
689 }
690
691 #[test]
692 fn apply_with_empty_list_clears_entry() {
693 let l = layer();
694 l.apply(ev(
695 "rust",
696 "file:///x.rs",
697 Some(1),
698 vec![diag(0, DiagnosticSeverity::ERROR, "first")],
699 ));
700 l.apply(ev("rust", "file:///x.rs", Some(2), Vec::new()));
701 let uri = Uri::from_str("file:///x.rs").unwrap();
702 assert!(l.diagnostics_for(&uri).is_empty());
703 assert!(l.iter_uris().is_empty(), "URI gone after clear");
704 }
705
706 #[test]
707 fn stale_version_is_dropped() {
708 let l = layer();
709 l.apply(ev(
710 "rust",
711 "file:///x.rs",
712 Some(5),
713 vec![diag(0, DiagnosticSeverity::ERROR, "current")],
714 ));
715 l.apply(ev(
717 "rust",
718 "file:///x.rs",
719 Some(3),
720 vec![diag(0, DiagnosticSeverity::ERROR, "stale")],
721 ));
722 let uri = Uri::from_str("file:///x.rs").unwrap();
723 let d = l.diagnostics_for(&uri);
724 assert_eq!(d.len(), 1);
725 assert_eq!(d[0].message, "current");
726 }
727
728 #[test]
729 fn equal_version_replaces_state() {
730 let l = layer();
731 l.apply(ev(
732 "rust",
733 "file:///x.rs",
734 Some(5),
735 vec![diag(0, DiagnosticSeverity::ERROR, "first")],
736 ));
737 l.apply(ev(
738 "rust",
739 "file:///x.rs",
740 Some(5),
741 vec![diag(0, DiagnosticSeverity::ERROR, "republish")],
742 ));
743 let uri = Uri::from_str("file:///x.rs").unwrap();
744 let d = l.diagnostics_for(&uri);
745 assert_eq!(d.len(), 1);
746 assert_eq!(d[0].message, "republish");
747 }
748
749 #[test]
750 fn version_none_always_accepted() {
751 let l = layer();
752 l.apply(ev(
753 "rust",
754 "file:///x.rs",
755 None,
756 vec![diag(0, DiagnosticSeverity::ERROR, "first")],
757 ));
758 l.apply(ev(
759 "rust",
760 "file:///x.rs",
761 None,
762 vec![diag(0, DiagnosticSeverity::WARNING, "second")],
763 ));
764 let uri = Uri::from_str("file:///x.rs").unwrap();
765 let d = l.diagnostics_for(&uri);
766 assert_eq!(d.len(), 1);
767 assert_eq!(d[0].message, "second");
768 }
769
770 #[test]
771 fn multi_server_diagnostics_merge_per_uri() {
772 let l = layer();
773 l.apply(ev(
774 "rust",
775 "file:///x.rs",
776 Some(1),
777 vec![diag(0, DiagnosticSeverity::ERROR, "rust err")],
778 ));
779 l.apply(ev(
780 "clippy",
781 "file:///x.rs",
782 Some(1),
783 vec![diag(2, DiagnosticSeverity::WARNING, "clippy warn")],
784 ));
785 let uri = Uri::from_str("file:///x.rs").unwrap();
786 let d = l.diagnostics_for(&uri);
787 assert_eq!(d.len(), 2);
788 let messages: Vec<&str> = d.iter().map(|x| x.message.as_str()).collect();
789 assert!(messages.contains(&"rust err"));
790 assert!(messages.contains(&"clippy warn"));
791 }
792
793 #[test]
794 fn one_servers_clear_does_not_drop_anothers_diagnostics() {
795 let l = layer();
796 l.apply(ev(
797 "rust",
798 "file:///x.rs",
799 Some(1),
800 vec![diag(0, DiagnosticSeverity::ERROR, "rust")],
801 ));
802 l.apply(ev(
803 "clippy",
804 "file:///x.rs",
805 Some(1),
806 vec![diag(2, DiagnosticSeverity::WARNING, "clippy")],
807 ));
808 l.apply(ev("rust", "file:///x.rs", Some(2), Vec::new()));
810 let uri = Uri::from_str("file:///x.rs").unwrap();
811 let d = l.diagnostics_for(&uri);
812 assert_eq!(d.len(), 1);
813 assert_eq!(d[0].message, "clippy");
814 }
815
816 #[test]
817 fn line_severity_returns_most_severe() {
818 let l = layer();
819 l.apply(ev(
820 "rust",
821 "file:///x.rs",
822 None,
823 vec![
824 diag(2, DiagnosticSeverity::WARNING, "warn"),
825 diag(2, DiagnosticSeverity::ERROR, "err"),
826 diag(2, DiagnosticSeverity::HINT, "hint"),
827 ],
828 ));
829 let uri = Uri::from_str("file:///x.rs").unwrap();
830 assert_eq!(l.line_severity(&uri, 2), Some(DiagnosticSeverity::ERROR));
831 }
832
833 #[test]
834 fn line_severity_returns_none_for_clean_lines() {
835 let l = layer();
836 l.apply(ev(
837 "rust",
838 "file:///x.rs",
839 None,
840 vec![diag(0, DiagnosticSeverity::ERROR, "boom")],
841 ));
842 let uri = Uri::from_str("file:///x.rs").unwrap();
843 assert_eq!(l.line_severity(&uri, 5), None);
844 }
845
846 #[test]
847 fn inline_summary_picks_most_severe_and_counts_extras() {
848 let l = layer();
849 l.apply(ev(
850 "rust",
851 "file:///x.rs",
852 None,
853 vec![
854 diag(2, DiagnosticSeverity::WARNING, "warn"),
855 diag(2, DiagnosticSeverity::ERROR, "the error"),
856 diag(2, DiagnosticSeverity::HINT, "hint"),
857 ],
858 ));
859 let uri = Uri::from_str("file:///x.rs").unwrap();
860 let s = l.inline_line_summary(&uri, 2, 3).unwrap();
863 assert_eq!(s.severity_rank, 0);
864 assert_eq!(s.text, "the error +2");
865 }
866
867 #[test]
868 fn inline_summary_filters_by_min_severity() {
869 let l = layer();
870 l.apply(ev(
871 "rust",
872 "file:///x.rs",
873 None,
874 vec![
875 diag(2, DiagnosticSeverity::WARNING, "warn"),
876 diag(2, DiagnosticSeverity::HINT, "hint"),
877 ],
878 ));
879 let uri = Uri::from_str("file:///x.rs").unwrap();
880 let s = l.inline_line_summary(&uri, 2, 1).unwrap();
883 assert_eq!(s.severity_rank, 1);
884 assert_eq!(s.text, "warn");
885 assert!(l.inline_line_summary(&uri, 2, 0).is_none());
887 }
888
889 #[test]
890 fn inline_summary_none_on_clean_line() {
891 let l = layer();
892 l.apply(ev(
893 "rust",
894 "file:///x.rs",
895 None,
896 vec![diag(0, DiagnosticSeverity::ERROR, "boom")],
897 ));
898 let uri = Uri::from_str("file:///x.rs").unwrap();
899 assert!(l.inline_line_summary(&uri, 5, 3).is_none());
900 }
901
902 #[test]
903 fn inline_summary_truncates_long_first_line() {
904 let long = "x".repeat(INLINE_SUMMARY_MAX_CHARS + 40);
905 let multi = format!("{long}\nsecond line that must be dropped");
906 let l = layer();
907 l.apply(ev(
908 "rust",
909 "file:///x.rs",
910 None,
911 vec![diag(1, DiagnosticSeverity::ERROR, &multi)],
912 ));
913 let uri = Uri::from_str("file:///x.rs").unwrap();
914 let s = l.inline_line_summary(&uri, 1, 3).unwrap();
915 assert_eq!(s.text.chars().count(), INLINE_SUMMARY_MAX_CHARS + 1);
917 assert!(s.text.ends_with('…'));
918 assert!(!s.text.contains("second line"));
919 }
920
921 #[test]
922 fn inline_summary_tie_keeps_earliest() {
923 let l = layer();
924 l.apply(ev(
925 "rust",
926 "file:///x.rs",
927 None,
928 vec![
929 diag(3, DiagnosticSeverity::ERROR, "first error"),
930 diag(3, DiagnosticSeverity::ERROR, "second error"),
931 ],
932 ));
933 let uri = Uri::from_str("file:///x.rs").unwrap();
934 let s = l.inline_line_summary(&uri, 3, 3).unwrap();
935 assert_eq!(s.text, "first error +1");
936 }
937
938 #[test]
939 fn diagnostics_on_line_filters_by_range() {
940 let mut d_multi = diag(2, DiagnosticSeverity::ERROR, "spans");
941 d_multi.range.end = Position {
942 line: 4,
943 character: 0,
944 };
945 let l = layer();
946 l.apply(ev("rust", "file:///x.rs", None, vec![d_multi]));
947 let uri = Uri::from_str("file:///x.rs").unwrap();
948 for line in 2..=4 {
950 assert_eq!(l.diagnostics_on_line(&uri, line).len(), 1, "line {line}");
951 }
952 assert!(l.diagnostics_on_line(&uri, 5).is_empty());
954 }
955
956 #[test]
957 fn iter_uris_sorts_alphabetically() {
958 let l = layer();
959 l.apply(ev(
960 "rust",
961 "file:///b.rs",
962 None,
963 vec![diag(0, DiagnosticSeverity::ERROR, "x")],
964 ));
965 l.apply(ev(
966 "rust",
967 "file:///a.rs",
968 None,
969 vec![diag(0, DiagnosticSeverity::ERROR, "x")],
970 ));
971 let uris = l.iter_uris();
972 assert_eq!(uris.len(), 2);
973 assert_eq!(uris[0].as_str(), "file:///a.rs");
974 assert_eq!(uris[1].as_str(), "file:///b.rs");
975 }
976
977 #[test]
978 fn snapshot_returns_per_uri_sorted_by_line_then_column() {
979 let l = layer();
980 let mut d_a = diag(0, DiagnosticSeverity::ERROR, "first");
981 d_a.range.start = Position {
982 line: 5,
983 character: 10,
984 };
985 let mut d_b = diag(0, DiagnosticSeverity::ERROR, "second");
986 d_b.range.start = Position {
987 line: 5,
988 character: 3,
989 };
990 let mut d_c = diag(0, DiagnosticSeverity::ERROR, "third");
991 d_c.range.start = Position {
992 line: 1,
993 character: 0,
994 };
995 l.apply(ev("rust", "file:///x.rs", None, vec![d_a, d_b, d_c]));
996 let snap = l.snapshot();
997 assert_eq!(snap.len(), 1);
998 let (_, diags) = &snap[0];
999 let messages: Vec<&str> = diags.iter().map(|d| d.message.as_str()).collect();
1000 assert_eq!(messages, vec!["third", "second", "first"]);
1002 }
1003
1004 #[test]
1005 fn severity_counts_tallies_across_uris() {
1006 let l = layer();
1007 l.apply(ev(
1008 "rust",
1009 "file:///a.rs",
1010 None,
1011 vec![
1012 diag(0, DiagnosticSeverity::ERROR, "e1"),
1013 diag(1, DiagnosticSeverity::WARNING, "w1"),
1014 ],
1015 ));
1016 l.apply(ev(
1017 "rust",
1018 "file:///b.rs",
1019 None,
1020 vec![
1021 diag(0, DiagnosticSeverity::ERROR, "e2"),
1022 diag(1, DiagnosticSeverity::HINT, "h1"),
1023 diag(2, DiagnosticSeverity::INFORMATION, "i1"),
1024 ],
1025 ));
1026 let c = l.severity_counts();
1027 assert_eq!(c.errors, 2);
1028 assert_eq!(c.warnings, 1);
1029 assert_eq!(c.info, 1);
1030 assert_eq!(c.hints, 1);
1031 assert_eq!(c.total(), 5);
1032 }
1033
1034 #[test]
1035 fn count_returns_total_across_servers_and_uris() {
1036 let l = layer();
1037 l.apply(ev(
1038 "rust",
1039 "file:///a.rs",
1040 None,
1041 vec![diag(0, DiagnosticSeverity::ERROR, "x")],
1042 ));
1043 l.apply(ev(
1044 "clippy",
1045 "file:///a.rs",
1046 None,
1047 vec![diag(1, DiagnosticSeverity::WARNING, "y")],
1048 ));
1049 l.apply(ev(
1050 "rust",
1051 "file:///b.rs",
1052 None,
1053 vec![diag(0, DiagnosticSeverity::ERROR, "z")],
1054 ));
1055 assert_eq!(l.count(), 3);
1056 }
1057
1058 #[test]
1059 fn clear_drops_everything() {
1060 let l = layer();
1061 l.apply(ev(
1062 "rust",
1063 "file:///x.rs",
1064 None,
1065 vec![diag(0, DiagnosticSeverity::ERROR, "x")],
1066 ));
1067 l.clear();
1068 assert_eq!(l.count(), 0);
1069 }
1070
1071 #[test]
1072 fn clear_uri_drops_only_that_uri() {
1073 let l = layer();
1074 l.apply(ev(
1075 "rust",
1076 "file:///a.rs",
1077 None,
1078 vec![diag(0, DiagnosticSeverity::ERROR, "a")],
1079 ));
1080 l.apply(ev(
1081 "rust",
1082 "file:///b.rs",
1083 None,
1084 vec![diag(0, DiagnosticSeverity::ERROR, "b")],
1085 ));
1086 let a = Uri::from_str("file:///a.rs").unwrap();
1087 l.clear_uri(&a);
1088 assert!(l.diagnostics_for(&a).is_empty());
1089 let b = Uri::from_str("file:///b.rs").unwrap();
1090 assert_eq!(l.diagnostics_for(&b).len(), 1);
1091 }
1092
1093 #[test]
1094 fn clear_server_drops_only_that_servers_entries() {
1095 let l = layer();
1096 l.apply(ev(
1097 "rust",
1098 "file:///x.rs",
1099 None,
1100 vec![diag(0, DiagnosticSeverity::ERROR, "rust")],
1101 ));
1102 l.apply(ev(
1103 "clippy",
1104 "file:///x.rs",
1105 None,
1106 vec![diag(0, DiagnosticSeverity::WARNING, "clippy")],
1107 ));
1108 let id: Arc<str> = Arc::from("rust");
1109 l.clear_server(&id);
1110 let uri = Uri::from_str("file:///x.rs").unwrap();
1111 let d = l.diagnostics_for(&uri);
1112 assert_eq!(d.len(), 1);
1113 assert_eq!(d[0].message, "clippy");
1114 }
1115
1116 #[test]
1117 fn clone_shares_state() {
1118 let l = layer();
1119 let l2 = l.clone();
1120 l.apply(ev(
1121 "rust",
1122 "file:///x.rs",
1123 None,
1124 vec![diag(0, DiagnosticSeverity::ERROR, "x")],
1125 ));
1126 assert_eq!(l2.count(), 1);
1127 }
1128}