1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::time::Instant;
4
5use crate::lsp::registry::ServerKind;
6use crate::lsp::roots::ServerKey;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct StoredDiagnostic {
11 pub file: PathBuf,
12 pub line: u32,
13 pub column: u32,
14 pub end_line: u32,
15 pub end_column: u32,
16 pub severity: DiagnosticSeverity,
17 pub message: String,
18 pub code: Option<String>,
19 pub source: Option<String>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum DiagnosticSeverity {
24 Error,
25 Warning,
26 Information,
27 Hint,
28}
29
30impl DiagnosticSeverity {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::Error => "error",
34 Self::Warning => "warning",
35 Self::Information => "information",
36 Self::Hint => "hint",
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
45pub struct DiagnosticEntry {
46 pub diagnostics: Vec<StoredDiagnostic>,
47 pub epoch: u64,
51 pub result_id: Option<String>,
54 pub version: Option<i32>,
62}
63
64pub struct DiagnosticsStore {
83 entries: HashMap<(ServerKey, PathBuf), DiagnosticEntry>,
85 order: Vec<(ServerKey, PathBuf)>,
88 capacity: usize,
90 next_epoch: u64,
92 last_publish_at_for_file: HashMap<(ServerKey, PathBuf), Instant>,
95}
96
97impl DiagnosticsStore {
98 pub fn new() -> Self {
99 Self::with_capacity(5000)
100 }
101
102 pub fn with_capacity(capacity: usize) -> Self {
103 Self {
104 entries: HashMap::new(),
105 order: Vec::new(),
106 capacity,
107 next_epoch: 0,
108 last_publish_at_for_file: HashMap::new(),
109 }
110 }
111
112 pub fn set_capacity(&mut self, capacity: usize) {
116 self.capacity = capacity;
117 if capacity > 0 {
118 while self.entries.len() > capacity {
119 self.evict_lru();
120 }
121 }
122 }
123
124 pub fn len(&self) -> usize {
126 self.entries.len()
127 }
128
129 pub fn is_empty(&self) -> bool {
130 self.entries.is_empty()
131 }
132
133 pub fn publish(
142 &mut self,
143 server: ServerKey,
144 file: PathBuf,
145 diagnostics: Vec<StoredDiagnostic>,
146 ) {
147 self.publish_with_result_id(server, file, diagnostics, None);
148 }
149
150 pub fn publish_with_result_id(
153 &mut self,
154 server: ServerKey,
155 file: PathBuf,
156 diagnostics: Vec<StoredDiagnostic>,
157 result_id: Option<String>,
158 ) {
159 self.publish_full(server, file, diagnostics, result_id, None);
160 }
161
162 pub fn publish_full(
166 &mut self,
167 server: ServerKey,
168 file: PathBuf,
169 diagnostics: Vec<StoredDiagnostic>,
170 result_id: Option<String>,
171 version: Option<i32>,
172 ) {
173 let key = (server, file);
174 self.next_epoch = self.next_epoch.saturating_add(1);
175 let entry = DiagnosticEntry {
176 diagnostics,
177 epoch: self.next_epoch,
178 result_id,
179 version,
180 };
181
182 self.last_publish_at_for_file
183 .insert(key.clone(), Instant::now());
184
185 if self.entries.contains_key(&key) {
186 self.entries.insert(key.clone(), entry);
187 self.touch_existing(&key);
188 } else {
189 if self.capacity > 0 && self.entries.len() >= self.capacity {
191 self.evict_lru();
192 }
193 self.entries.insert(key.clone(), entry);
194 self.order.push(key);
195 }
196 }
197
198 pub fn publish_with_kind(
204 &mut self,
205 kind: ServerKind,
206 file: PathBuf,
207 diagnostics: Vec<StoredDiagnostic>,
208 ) {
209 let key = ServerKey {
210 kind,
211 root: PathBuf::new(),
212 };
213 self.publish(key, file, diagnostics);
214 }
215
216 pub fn for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
219 self.entries
220 .iter()
221 .filter(|((_, stored_file), _)| stored_file == file)
222 .flat_map(|(_, entry)| entry.diagnostics.iter())
223 .collect()
224 }
225
226 pub fn entries_for_file(&self, file: &Path) -> Vec<(&ServerKey, &DiagnosticEntry)> {
229 self.entries
230 .iter()
231 .filter(|((_, stored_file), _)| stored_file == file)
232 .map(|((key, _), entry)| (key, entry))
233 .collect()
234 }
235
236 pub fn has_any_report_for_file(&self, file: &Path) -> bool {
238 self.entries.keys().any(|(_, f)| f == file)
239 }
240
241 pub fn has_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
244 self.entries
245 .contains_key(&(server.clone(), file.to_path_buf()))
246 }
247
248 pub fn has_publish_for_file_after(
252 &self,
253 server: &ServerKey,
254 file: &Path,
255 since: Instant,
256 ) -> bool {
257 self.last_publish_at_for_file
258 .get(&(server.clone(), file.to_path_buf()))
259 .is_some_and(|published_at| *published_at >= since)
260 }
261
262 pub fn for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
264 self.entries
265 .iter()
266 .filter(|((_, stored_file), _)| stored_file.starts_with(dir))
267 .flat_map(|(_, entry)| entry.diagnostics.iter())
268 .collect()
269 }
270
271 pub fn all(&self) -> Vec<&StoredDiagnostic> {
273 self.entries
274 .values()
275 .flat_map(|entry| entry.diagnostics.iter())
276 .collect()
277 }
278
279 pub fn error_warning_counts(&self) -> (usize, usize) {
285 let mut errors = 0usize;
286 let mut warnings = 0usize;
287 for entry in self.entries.values() {
288 for diagnostic in &entry.diagnostics {
289 match diagnostic.severity {
290 DiagnosticSeverity::Error => errors += 1,
291 DiagnosticSeverity::Warning => warnings += 1,
292 _ => {}
293 }
294 }
295 }
296 (errors, warnings)
297 }
298
299 pub fn filtered_error_warning_counts(
311 &self,
312 mut keep: impl FnMut(&Path) -> bool,
313 ) -> (usize, usize) {
314 let mut seen: std::collections::HashSet<(
318 &Path,
319 u32,
320 u32,
321 u32,
322 u32,
323 &str,
324 &str,
325 Option<&str>,
326 )> = std::collections::HashSet::new();
327 let mut errors = 0usize;
328 let mut warnings = 0usize;
329 for ((_, file), entry) in &self.entries {
330 if !keep(file) {
334 continue;
335 }
336 for diagnostic in &entry.diagnostics {
337 let dedup_key = (
338 diagnostic.file.as_path(),
339 diagnostic.line,
340 diagnostic.column,
341 diagnostic.end_line,
342 diagnostic.end_column,
343 diagnostic.severity.as_str(),
344 diagnostic.message.as_str(),
345 diagnostic.source.as_deref(),
346 );
347 if !seen.insert(dedup_key) {
348 continue;
349 }
350 match diagnostic.severity {
351 DiagnosticSeverity::Error => errors += 1,
352 DiagnosticSeverity::Warning => warnings += 1,
353 _ => {}
354 }
355 }
356 }
357 (errors, warnings)
358 }
359
360 pub fn clear_server(&mut self, server: ServerKind) {
364 self.entries
365 .retain(|(stored_key, _), _| stored_key.kind != server);
366 self.order
367 .retain(|(stored_key, _)| stored_key.kind != server);
368 self.last_publish_at_for_file
369 .retain(|(stored_key, _), _| stored_key.kind != server);
370 }
371
372 pub fn clear_for_server_file(&mut self, key: &ServerKey, file: &Path) {
374 let cache_key = (key.clone(), file.to_path_buf());
375 self.entries.remove(&cache_key);
376 self.order.retain(|entry_key| entry_key != &cache_key);
377 self.last_publish_at_for_file.remove(&cache_key);
378 }
379
380 pub fn clear_for_file(&mut self, file: &Path) -> bool {
386 let before = self.entries.len();
387 self.entries
388 .retain(|(_, stored_file), _| stored_file != file);
389 let removed = self.entries.len() != before;
390 if removed {
391 self.order.retain(|(_, stored_file)| stored_file != file);
392 self.last_publish_at_for_file
393 .retain(|(_, stored_file), _| stored_file != file);
394 }
395 removed
396 }
397
398 pub fn clear_for_server(&mut self, key: &ServerKey) {
400 self.entries.retain(|(k, _), _| k != key);
401 self.order.retain(|(k, _)| k != key);
402 self.last_publish_at_for_file.retain(|(k, _), _| k != key);
403 }
404
405 pub fn clear_server_instance(&mut self, key: &ServerKey) {
408 self.clear_for_server(key);
409 }
410
411 fn evict_lru(&mut self) -> Option<(ServerKey, PathBuf)> {
413 if self.order.is_empty() {
414 return None;
415 }
416 let evicted = self.order.remove(0);
417 self.entries.remove(&evicted);
418 self.last_publish_at_for_file.remove(&evicted);
419 Some(evicted)
420 }
421
422 fn touch_existing(&mut self, key: &(ServerKey, PathBuf)) {
423 if let Some(idx) = self.order.iter().position(|k| k == key) {
424 let removed = self.order.remove(idx);
425 self.order.push(removed);
426 }
427 }
428}
429
430impl Default for DiagnosticsStore {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436pub fn from_lsp_diagnostics(
439 file: PathBuf,
440 lsp_diagnostics: Vec<lsp_types::Diagnostic>,
441) -> Vec<StoredDiagnostic> {
442 lsp_diagnostics
443 .into_iter()
444 .map(|diagnostic| StoredDiagnostic {
445 file: file.clone(),
446 line: diagnostic.range.start.line + 1,
447 column: diagnostic.range.start.character + 1,
448 end_line: diagnostic.range.end.line + 1,
449 end_column: diagnostic.range.end.character + 1,
450 severity: match diagnostic.severity {
451 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
452 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
453 Some(lsp_types::DiagnosticSeverity::INFORMATION) => DiagnosticSeverity::Information,
454 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
455 _ => DiagnosticSeverity::Warning,
456 },
457 message: diagnostic.message,
458 code: diagnostic.code.map(|code| match code {
459 lsp_types::NumberOrString::Number(value) => value.to_string(),
460 lsp_types::NumberOrString::String(value) => value,
461 }),
462 source: diagnostic.source,
463 })
464 .collect()
465}
466
467#[cfg(test)]
468mod tests {
469 use std::path::{Path, PathBuf};
470
471 use lsp_types::{
472 Diagnostic, DiagnosticSeverity as LspDiagnosticSeverity, NumberOrString, Position, Range,
473 };
474
475 use super::{from_lsp_diagnostics, DiagnosticSeverity, DiagnosticsStore, StoredDiagnostic};
476 use crate::lsp::registry::ServerKind;
477 use crate::lsp::roots::ServerKey;
478
479 fn server_key(kind: ServerKind) -> ServerKey {
480 ServerKey {
481 kind,
482 root: PathBuf::from("/tmp/repo"),
483 }
484 }
485
486 fn diag(file: &str, line: u32, msg: &str, sev: DiagnosticSeverity) -> StoredDiagnostic {
487 StoredDiagnostic {
488 file: PathBuf::from(file),
489 line,
490 column: 1,
491 end_line: line,
492 end_column: 2,
493 severity: sev,
494 message: msg.into(),
495 code: None,
496 source: None,
497 }
498 }
499
500 #[test]
501 fn converts_lsp_positions_to_one_based() {
502 let file = PathBuf::from("/tmp/demo.rs");
503 let diagnostics = from_lsp_diagnostics(
504 file.clone(),
505 vec![Diagnostic {
506 range: Range::new(Position::new(0, 0), Position::new(1, 4)),
507 severity: Some(LspDiagnosticSeverity::ERROR),
508 code: Some(NumberOrString::String("E1".into())),
509 code_description: None,
510 source: Some("fake".into()),
511 message: "boom".into(),
512 related_information: None,
513 tags: None,
514 data: None,
515 }],
516 );
517
518 assert_eq!(diagnostics.len(), 1);
519 assert_eq!(diagnostics[0].file, file);
520 assert_eq!(diagnostics[0].line, 1);
521 assert_eq!(diagnostics[0].column, 1);
522 assert_eq!(diagnostics[0].end_line, 2);
523 assert_eq!(diagnostics[0].end_column, 5);
524 assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Error);
525 assert_eq!(diagnostics[0].code.as_deref(), Some("E1"));
526 }
527
528 #[test]
529 fn publish_replaces_existing_file_diagnostics() {
530 let file = PathBuf::from("/tmp/demo.rs");
531 let mut store = DiagnosticsStore::new();
532 let key = server_key(ServerKind::Rust);
533
534 store.publish(
535 key.clone(),
536 file.clone(),
537 vec![diag(
538 "/tmp/demo.rs",
539 1,
540 "first",
541 DiagnosticSeverity::Warning,
542 )],
543 );
544 store.publish(
545 key.clone(),
546 file.clone(),
547 vec![diag("/tmp/demo.rs", 2, "second", DiagnosticSeverity::Error)],
548 );
549
550 let stored = store.for_file(&file);
551 assert_eq!(stored.len(), 1);
552 assert_eq!(stored[0].message, "second");
553 }
554
555 #[test]
556 fn empty_publish_is_preserved_as_checked_clean() {
557 let file = PathBuf::from("/tmp/clean.rs");
561 let mut store = DiagnosticsStore::new();
562 let key = server_key(ServerKind::Rust);
563
564 store.publish(
566 key.clone(),
567 file.clone(),
568 vec![diag(
569 "/tmp/clean.rs",
570 5,
571 "fix me",
572 DiagnosticSeverity::Warning,
573 )],
574 );
575 assert!(store.has_any_report_for_file(&file));
576 assert_eq!(store.for_file(&file).len(), 1);
577
578 store.publish(key.clone(), file.clone(), Vec::new());
581 assert!(
582 store.has_any_report_for_file(&file),
583 "checked-clean must be distinguishable from never-checked"
584 );
585 assert_eq!(store.for_file(&file).len(), 0);
586
587 let entries = store.entries_for_file(&file);
588 assert_eq!(entries.len(), 1);
589 assert!(entries[0].1.epoch > 0);
590 }
591
592 #[test]
593 fn never_checked_returns_no_report() {
594 let store = DiagnosticsStore::new();
595 let file = PathBuf::from("/tmp/never.rs");
596 assert!(!store.has_any_report_for_file(&file));
597 assert!(store.for_file(&file).is_empty());
598 }
599
600 #[test]
601 fn per_server_state_is_tracked_independently() {
602 let file = PathBuf::from("/tmp/multi.py");
603 let mut store = DiagnosticsStore::new();
604 let pyright_key = server_key(ServerKind::Python);
605 let ty_key = server_key(ServerKind::Ty);
606
607 store.publish(
608 pyright_key,
609 file.clone(),
610 vec![diag(
611 "/tmp/multi.py",
612 1,
613 "pyright says X",
614 DiagnosticSeverity::Error,
615 )],
616 );
617 store.publish(
618 ty_key,
619 file.clone(),
620 vec![diag(
621 "/tmp/multi.py",
622 2,
623 "ty says Y",
624 DiagnosticSeverity::Warning,
625 )],
626 );
627
628 let messages: Vec<&str> = store
629 .for_file(&file)
630 .into_iter()
631 .map(|d| d.message.as_str())
632 .collect();
633
634 assert_eq!(messages.len(), 2, "both servers' reports preserved");
635 assert!(messages.iter().any(|m| m == &"pyright says X"));
636 assert!(messages.iter().any(|m| m == &"ty says Y"));
637 }
638
639 #[test]
640 fn clear_for_server_file_removes_only_exact_entry() {
641 let file_a = PathBuf::from("/tmp/a.rs");
642 let file_b = PathBuf::from("/tmp/b.rs");
643 let mut store = DiagnosticsStore::new();
644 let rust_key = server_key(ServerKind::Rust);
645 let py_key = server_key(ServerKind::Python);
646
647 store.publish(
648 rust_key.clone(),
649 file_a.clone(),
650 vec![diag("/tmp/a.rs", 1, "rust a", DiagnosticSeverity::Error)],
651 );
652 store.publish(
653 rust_key.clone(),
654 file_b.clone(),
655 vec![diag("/tmp/b.rs", 1, "rust b", DiagnosticSeverity::Warning)],
656 );
657 store.publish(
658 py_key.clone(),
659 file_a.clone(),
660 vec![diag("/tmp/a.rs", 2, "py a", DiagnosticSeverity::Warning)],
661 );
662
663 store.clear_for_server_file(&rust_key, &file_a);
664
665 assert!(!store.has_report_for_server_file(&rust_key, &file_a));
666 assert!(store.has_report_for_server_file(&rust_key, &file_b));
667 assert!(store.has_report_for_server_file(&py_key, &file_a));
668 }
669
670 #[test]
671 fn lru_evicts_oldest_when_capacity_exceeded() {
672 let mut store = DiagnosticsStore::with_capacity(2);
673 let key = server_key(ServerKind::Rust);
674
675 store.publish(
676 key.clone(),
677 PathBuf::from("/a.rs"),
678 vec![diag("/a.rs", 1, "a", DiagnosticSeverity::Warning)],
679 );
680 store.publish(
681 key.clone(),
682 PathBuf::from("/b.rs"),
683 vec![diag("/b.rs", 1, "b", DiagnosticSeverity::Warning)],
684 );
685 assert_eq!(store.len(), 2);
686
687 store.publish(
689 key.clone(),
690 PathBuf::from("/c.rs"),
691 vec![diag("/c.rs", 1, "c", DiagnosticSeverity::Warning)],
692 );
693 assert_eq!(store.len(), 2);
694 assert!(!store.has_any_report_for_file(Path::new("/a.rs")));
695 assert!(store.has_any_report_for_file(Path::new("/b.rs")));
696 assert!(store.has_any_report_for_file(Path::new("/c.rs")));
697 }
698
699 #[test]
700 fn touching_existing_entry_moves_it_to_end_of_lru() {
701 let mut store = DiagnosticsStore::with_capacity(2);
702 let key = server_key(ServerKind::Rust);
703
704 store.publish(
705 key.clone(),
706 PathBuf::from("/a.rs"),
707 vec![diag("/a.rs", 1, "a", DiagnosticSeverity::Warning)],
708 );
709 store.publish(
710 key.clone(),
711 PathBuf::from("/b.rs"),
712 vec![diag("/b.rs", 1, "b", DiagnosticSeverity::Warning)],
713 );
714
715 store.publish(
718 key.clone(),
719 PathBuf::from("/a.rs"),
720 vec![diag("/a.rs", 1, "a2", DiagnosticSeverity::Error)],
721 );
722 store.publish(
723 key.clone(),
724 PathBuf::from("/c.rs"),
725 vec![diag("/c.rs", 1, "c", DiagnosticSeverity::Warning)],
726 );
727
728 assert!(store.has_any_report_for_file(Path::new("/a.rs")));
729 assert!(!store.has_any_report_for_file(Path::new("/b.rs")));
730 assert!(store.has_any_report_for_file(Path::new("/c.rs")));
731 }
732
733 #[test]
734 fn capacity_zero_disables_eviction() {
735 let mut store = DiagnosticsStore::with_capacity(0);
736 let key = server_key(ServerKind::Rust);
737
738 for i in 0..50 {
739 store.publish(
740 key.clone(),
741 PathBuf::from(format!("/f{i}.rs")),
742 vec![diag(
743 &format!("/f{i}.rs"),
744 1,
745 "x",
746 DiagnosticSeverity::Warning,
747 )],
748 );
749 }
750 assert_eq!(store.len(), 50);
751 }
752
753 #[test]
754 fn set_capacity_evicts_on_shrink() {
755 let mut store = DiagnosticsStore::with_capacity(0);
756 let key = server_key(ServerKind::Rust);
757 for i in 0..10 {
758 store.publish(
759 key.clone(),
760 PathBuf::from(format!("/f{i}.rs")),
761 vec![diag(
762 &format!("/f{i}.rs"),
763 1,
764 "x",
765 DiagnosticSeverity::Warning,
766 )],
767 );
768 }
769 assert_eq!(store.len(), 10);
770
771 store.set_capacity(3);
772 assert_eq!(store.len(), 3);
773 assert!(store.has_any_report_for_file(Path::new("/f9.rs")));
775 assert!(!store.has_any_report_for_file(Path::new("/f0.rs")));
776 }
777
778 #[test]
779 fn epoch_increments_monotonically() {
780 let mut store = DiagnosticsStore::new();
781 let key = server_key(ServerKind::Rust);
782 let file = PathBuf::from("/e.rs");
783
784 store.publish(key.clone(), file.clone(), Vec::new());
785 let e1 = store.entries_for_file(&file)[0].1.epoch;
786
787 store.publish(key.clone(), file.clone(), Vec::new());
788 let e2 = store.entries_for_file(&file)[0].1.epoch;
789
790 assert!(e2 > e1, "epoch must increase on republish");
791 }
792
793 #[test]
794 fn result_id_is_round_tripped() {
795 let mut store = DiagnosticsStore::new();
796 let key = server_key(ServerKind::Rust);
797 let file = PathBuf::from("/r.rs");
798
799 store.publish_with_result_id(
800 key.clone(),
801 file.clone(),
802 Vec::new(),
803 Some("rev-42".to_string()),
804 );
805
806 let entries = store.entries_for_file(&file);
807 assert_eq!(entries[0].1.result_id.as_deref(), Some("rev-42"));
808 }
809
810 #[test]
811 fn clear_server_drops_all_entries_for_kind() {
812 let mut store = DiagnosticsStore::new();
813 let py_key = server_key(ServerKind::Python);
814 let rust_key = server_key(ServerKind::Rust);
815
816 store.publish(
817 py_key.clone(),
818 PathBuf::from("/a.py"),
819 vec![diag("/a.py", 1, "x", DiagnosticSeverity::Error)],
820 );
821 store.publish(
822 rust_key.clone(),
823 PathBuf::from("/b.rs"),
824 vec![diag("/b.rs", 1, "y", DiagnosticSeverity::Error)],
825 );
826
827 store.clear_server(ServerKind::Python);
828 assert!(!store.has_any_report_for_file(Path::new("/a.py")));
829 assert!(store.has_any_report_for_file(Path::new("/b.rs")));
830 }
831
832 #[test]
833 fn clear_for_file_drops_every_server_entry_and_updates_counts() {
834 let mut store = DiagnosticsStore::new();
835 let py_key = server_key(ServerKind::Python);
836 let biome_key = server_key(ServerKind::Biome);
837
838 store.publish(
841 py_key,
842 PathBuf::from("/gone.ts"),
843 vec![diag("/gone.ts", 4, "type error", DiagnosticSeverity::Error)],
844 );
845 store.publish(
846 biome_key,
847 PathBuf::from("/gone.ts"),
848 vec![diag(
849 "/gone.ts",
850 7,
851 "lint warning",
852 DiagnosticSeverity::Warning,
853 )],
854 );
855 store.publish(
856 server_key(ServerKind::Rust),
857 PathBuf::from("/keep.rs"),
858 vec![diag("/keep.rs", 1, "live error", DiagnosticSeverity::Error)],
859 );
860
861 assert_eq!(store.error_warning_counts(), (2, 1));
862
863 let removed = store.clear_for_file(Path::new("/gone.ts"));
865 assert!(removed);
866 assert!(!store.has_any_report_for_file(Path::new("/gone.ts")));
867 assert!(store.has_any_report_for_file(Path::new("/keep.rs")));
869 assert_eq!(store.error_warning_counts(), (1, 0));
870
871 assert!(!store.clear_for_file(Path::new("/gone.ts")));
873 }
874
875 #[test]
876 fn filtered_counts_apply_keep_predicate() {
877 let mut store = DiagnosticsStore::new();
878 store.publish(
879 server_key(ServerKind::TypeScript),
880 PathBuf::from("/repo/src/app.ts"),
881 vec![diag(
882 "/repo/src/app.ts",
883 1,
884 "in build",
885 DiagnosticSeverity::Error,
886 )],
887 );
888 store.publish(
889 server_key(ServerKind::TypeScript),
890 PathBuf::from("/repo/src/app.test.ts"),
891 vec![diag(
892 "/repo/src/app.test.ts",
893 1,
894 "excluded",
895 DiagnosticSeverity::Error,
896 )],
897 );
898
899 assert_eq!(store.error_warning_counts(), (2, 0));
901 let counts = store.filtered_error_warning_counts(|file| !file.ends_with("app.test.ts"));
903 assert_eq!(counts, (1, 0));
904 }
905
906 #[test]
907 fn filtered_counts_dedup_across_servers() {
908 let mut store = DiagnosticsStore::new();
909 let file = "/repo/src/app.ts";
910 store.publish(
915 server_key(ServerKind::TypeScript),
916 PathBuf::from(file),
917 vec![diag(file, 7, "dup", DiagnosticSeverity::Error)],
918 );
919 store.publish(
920 server_key(ServerKind::Biome),
921 PathBuf::from(file),
922 vec![diag(file, 7, "dup", DiagnosticSeverity::Error)],
923 );
924
925 assert_eq!(store.error_warning_counts(), (2, 0));
926 assert_eq!(store.filtered_error_warning_counts(|_| true), (1, 0));
927 }
928
929 #[test]
930 fn filtered_counts_keep_distinct_diagnostics_same_file() {
931 let mut store = DiagnosticsStore::new();
932 let file = "/repo/src/app.ts";
933 store.publish(
936 server_key(ServerKind::TypeScript),
937 PathBuf::from(file),
938 vec![diag(file, 7, "type error", DiagnosticSeverity::Error)],
939 );
940 store.publish(
941 server_key(ServerKind::Biome),
942 PathBuf::from(file),
943 vec![diag(file, 12, "lint warn", DiagnosticSeverity::Warning)],
944 );
945 assert_eq!(store.filtered_error_warning_counts(|_| true), (1, 1));
946 }
947}