1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
6use std::sync::Arc;
7
8use chrono::{DateTime, Utc};
9use lsp_types::{Diagnostic as LspDiagnostic, Uri};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use tracing::{debug, warn};
13
14use crate::bridge::indexing::{IndexingPolicy, IndexingReset, IndexingState, IndexingTracker};
15use crate::bridge::resources::{DiagnosticsResourceUri, PublishedDiagnosticsUri};
16use crate::bridge::{
17 DiagnosticsAvailability, DocumentTracker, DocumentVersion, Opening, uri_to_path,
18};
19use crate::config::{BoundedText, ServerId};
20use crate::util::{WarnLimiter, truncate_string};
21
22mod bounds;
23mod pulled_index;
24
25pub use bounds::BoundedDiagnostics;
26use bounds::{MAX_ENTRY_TEXT_BYTES, cap_diagnostics_entry_size};
27use pulled_index::{PulledIndex, point};
28pub use pulled_index::{ReportedSeverity, reported_code};
29
30const MAX_LOG_ENTRIES: usize = 100;
32
33pub const MAX_DIAGNOSTIC_ENTRIES: usize = 1000;
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
69pub struct DiagnosticsKey(String);
70
71impl DiagnosticsKey {
72 pub(crate) fn of(uri: &Uri) -> Self {
73 let text: &str = uri.as_ref();
74 if cfg!(windows) {
75 Self(text.to_ascii_lowercase())
76 } else {
77 Self(text.to_owned())
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84enum Spelling {
85 Canonical,
87 Alias(DiagnosticsKey),
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
93enum Provenance {
94 Pushed,
96 Pulled,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
105struct SlotKey {
106 uri: DiagnosticsKey,
107 provenance: Provenance,
108}
109
110impl SlotKey {
111 const fn pushed(uri: DiagnosticsKey) -> Self {
112 Self {
113 uri,
114 provenance: Provenance::Pushed,
115 }
116 }
117
118 const fn pulled(uri: DiagnosticsKey) -> Self {
119 Self {
120 uri,
121 provenance: Provenance::Pulled,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
128struct PullTicket(u64);
129
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
133struct ClearEpoch(u64);
134
135#[derive(Debug, Clone, Copy)]
139pub struct PullStamp {
140 ticket: PullTicket,
141 epoch: ClearEpoch,
142 version: DocumentVersion,
143}
144
145impl PullStamp {
146 pub(crate) const fn version(self) -> DocumentVersion {
148 self.version
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum VersionCheck {
155 Current,
157 Moved,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum Discard {
164 OlderTicket,
166 ServerCleared,
168 VersionMoved,
170 NoRoom,
172}
173
174#[derive(Debug)]
176pub enum SlotChange {
177 Identical,
179 Replaced { before: Option<DiagnosticSources> },
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum SnapshotBefore {
189 Take,
191 Skip,
193}
194
195#[derive(Debug)]
197struct NoRoom {
198 evicted: Vec<DiagnosticsKey>,
200}
201
202#[derive(Debug)]
204#[must_use]
205pub enum PullWrite {
206 Stored {
208 slot: SlotChange,
209 evicted: Vec<DiagnosticsKey>,
211 },
212 Discarded {
214 reason: Discard,
215 evicted: Vec<DiagnosticsKey>,
217 items: BoundedDiagnostics,
218 },
219}
220
221#[derive(Debug)]
223#[must_use]
224pub enum PushWrite {
225 Stored {
227 evicted: Vec<DiagnosticsKey>,
229 },
230 Dropped {
233 evicted: Vec<DiagnosticsKey>,
235 },
236}
237
238impl PushWrite {
239 #[must_use]
241 pub fn evicted(&self) -> &[DiagnosticsKey] {
242 match self {
243 Self::Stored { evicted } | Self::Dropped { evicted } => evicted,
244 }
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub enum ChangeOutcome {
251 Changed,
253 Unchanged,
255}
256
257impl ChangeOutcome {
258 pub(crate) fn of(before: Option<&DiagnosticInfo>, after: Option<&DiagnosticInfo>) -> Self {
261 Self::compare(before, after, |d| serde_json::to_vec(d).ok())
262 }
263
264 fn compare(
268 before: Option<&DiagnosticInfo>,
269 after: Option<&DiagnosticInfo>,
270 serialize: impl Fn(&LspDiagnostic) -> Option<Vec<u8>>,
271 ) -> Self {
272 let fingerprint = |info: &DiagnosticInfo| -> Option<Vec<Vec<u8>>> {
273 let mut items = info
274 .diagnostics
275 .iter()
276 .map(&serialize)
277 .collect::<Option<Vec<_>>>()?;
278 items.sort_unstable();
279 Some(items)
280 };
281 match (before, after) {
282 (None, None) => Self::Unchanged,
283 (Some(before), Some(after)) => match (fingerprint(before), fingerprint(after)) {
284 (Some(before), Some(after)) if before == after => Self::Unchanged,
285 _ => Self::Changed,
286 },
287 _ => Self::Changed,
288 }
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum DocumentSync {
295 Synced(DocumentVersion),
297 NotOpen,
299 Unattached,
301}
302
303fn pull_is_superseded(
317 pulled: DocumentVersion,
318 pushed: Option<DocumentVersion>,
319 sync: DocumentSync,
320) -> bool {
321 match (pushed, sync) {
322 (Some(pushed), _) if pushed > pulled => true,
323 (_, DocumentSync::Unattached) => false,
324 (_, DocumentSync::NotOpen) => true,
325 (_, DocumentSync::Synced(current)) => current != pulled,
326 }
327}
328
329#[derive(Debug, Clone, Copy)]
331enum Protect<'a> {
332 Nothing,
333 File(&'a DiagnosticsKey),
334}
335
336#[derive(Debug, Clone, Default, PartialEq, Eq)]
339struct Covered(BTreeSet<usize>);
340
341const MAX_LEARNED_PULL_SOURCES: usize = 32;
343
344const MAX_DIAGNOSTIC_SOURCE_BYTES: usize = 128;
346
347#[derive(Debug, Clone, PartialEq, Eq)]
350struct DiagnosticSource(BoundedText<MAX_DIAGNOSTIC_SOURCE_BYTES>);
351
352impl DiagnosticSource {
353 fn of(diagnostic: &LspDiagnostic) -> Option<Self> {
354 let text = diagnostic.source.as_deref()?;
355 BoundedText::new(text).ok().map(Self)
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
365enum Freshness {
366 Current,
369 Superseded { covered: Covered },
373}
374
375impl Freshness {
376 fn excludes(&self, index: usize) -> bool {
377 match self {
378 Self::Current => false,
379 Self::Superseded { covered } => covered.0.contains(&index),
380 }
381 }
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
387enum Origin {
388 Pushed {
390 spelling: Spelling,
391 version: Option<DocumentVersion>,
393 opening: Option<Opening>,
396 covered: Covered,
399 },
400 Pulled {
403 ticket: PullTicket,
405 opening: Option<Opening>,
408 version: DocumentVersion,
410 },
411}
412
413impl Origin {
414 const fn version(&self) -> Option<DocumentVersion> {
416 match self {
417 Self::Pushed { version, .. } => *version,
418 Self::Pulled { version, .. } => Some(*version),
419 }
420 }
421
422 const fn provenance(&self) -> Provenance {
423 match self {
424 Self::Pushed { .. } => Provenance::Pushed,
425 Self::Pulled { .. } => Provenance::Pulled,
426 }
427 }
428}
429
430#[derive(Debug)]
433struct CachedEntry {
434 uri: Uri,
436 diagnostics: Vec<LspDiagnostic>,
437 owner: ServerId,
439 seq: u64,
441 origin: Origin,
442}
443
444impl CachedEntry {
445 fn info(&self) -> DiagnosticInfo {
447 DiagnosticInfo {
448 uri: self.uri.clone(),
449 version: self.origin.version(),
450 diagnostics: self.diagnostics.clone(),
451 }
452 }
453
454 const fn file<'a>(&'a self, key: &'a SlotKey) -> &'a DiagnosticsKey {
456 match &self.origin {
457 Origin::Pushed {
458 spelling: Spelling::Alias(canonical),
459 ..
460 } => canonical,
461 Origin::Pushed {
462 spelling: Spelling::Canonical,
463 ..
464 }
465 | Origin::Pulled { .. } => &key.uri,
466 }
467 }
468}
469
470pub fn diagnostics_cache_key(uri: &DiagnosticsResourceUri) -> Option<DiagnosticsKey> {
474 let path = crate::bridge::resources::parse_uri(uri.as_str()).ok()?;
475 let lsp_uri = crate::bridge::try_path_to_uri(path.as_path())?;
476 Some(DiagnosticsKey::of(&lsp_uri))
477}
478
479const MAX_SOURCES_PER_FILE: usize = 8;
483
484const MAX_SERVER_MESSAGES: usize = 50;
486
487const MAX_RECENT_EVICTIONS: usize = MAX_DIAGNOSTIC_ENTRIES;
491
492const MAX_EVICTION_MARKS: usize = MAX_DIAGNOSTIC_ENTRIES;
494
495const EVICTION_REPLAY_WINDOW: std::time::Duration = std::time::Duration::from_mins(2);
497
498#[derive(Debug, Default)]
507struct EvictionRecord {
508 at: HashMap<DiagnosticsKey, std::time::Instant>,
509 order: VecDeque<(DiagnosticsKey, std::time::Instant)>,
510 overflow_warn: WarnLimiter,
511}
512
513impl EvictionRecord {
514 fn record(&mut self, file: DiagnosticsKey, now: std::time::Instant) {
515 self.prune_expired(now);
516 self.at.insert(file.clone(), now);
517 self.order.push_back((file, now));
518 while self.at.len() > MAX_RECENT_EVICTIONS {
519 self.forget_oldest(now);
520 }
521 if self.order.len() > self.at.len().saturating_mul(2) {
522 self.compact();
523 }
524 }
525
526 fn contains(&self, key: &DiagnosticsKey, now: std::time::Instant) -> bool {
527 self.at
528 .get(key)
529 .is_some_and(|at| now.saturating_duration_since(*at) <= EVICTION_REPLAY_WINDOW)
530 }
531
532 fn is_live(&self, key: &DiagnosticsKey, at: std::time::Instant) -> bool {
533 self.at.get(key) == Some(&at)
534 }
535
536 fn prune_expired(&mut self, now: std::time::Instant) {
537 while let Some((key, at)) = self
538 .order
539 .pop_front_if(|(_, at)| now.saturating_duration_since(*at) > EVICTION_REPLAY_WINDOW)
540 {
541 if self.is_live(&key, at) {
542 self.at.remove(&key);
543 }
544 }
545 }
546
547 fn forget_oldest(&mut self, now: std::time::Instant) {
548 while let Some((key, at)) = self.order.pop_front() {
549 if self.is_live(&key, at) {
550 self.at.remove(&key);
551 self.warn_overflow(now);
552 return;
553 }
554 }
555 }
556
557 fn warn_overflow(&mut self, now: std::time::Instant) {
558 if self.overflow_warn.due(now, EVICTION_REPLAY_WINDOW) {
559 warn!(
560 "more than {MAX_RECENT_EVICTIONS} diagnostics clears evicted within {}s, \
561 forgetting the oldest; a re-attaching listen may miss them",
562 EVICTION_REPLAY_WINDOW.as_secs()
563 );
564 }
565 }
566
567 fn compact(&mut self) {
568 let mut live: Vec<_> = self.at.iter().map(|(k, at)| (k.clone(), *at)).collect();
569 live.sort_by_key(|(_, at)| *at);
570 self.order = live.into();
571 }
572
573 #[cfg(test)]
574 fn len(&self) -> usize {
575 self.at.len()
576 }
577
578 #[cfg(test)]
579 fn is_empty(&self) -> bool {
580 self.at.is_empty()
581 }
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586enum EvictedContent {
587 Clean,
589 Lost,
591}
592
593#[derive(Debug)]
594struct EvictionMark {
595 owner: ServerId,
596 content: EvictedContent,
597 seq: u64,
598}
599
600#[derive(Debug, Default)]
611struct EvictionMarks {
612 by_file: HashMap<DiagnosticsKey, EvictionMark>,
613 order: BTreeMap<u64, DiagnosticsKey>,
614 next_seq: u64,
615 overflowed: HashSet<ServerId>,
616}
617
618impl EvictionMarks {
619 fn record(&mut self, file: DiagnosticsKey, owner: ServerId, content: EvictedContent) {
620 let content = match self.by_file.get(&file) {
621 Some(previous) if previous.content == EvictedContent::Lost => EvictedContent::Lost,
622 _ => content,
623 };
624 self.forget_file(&file);
625 let seq = self.next_seq;
626 self.next_seq = self.next_seq.saturating_add(1);
627 self.order.insert(seq, file.clone());
628 self.by_file.insert(
629 file,
630 EvictionMark {
631 owner,
632 content,
633 seq,
634 },
635 );
636 while self.by_file.len() > MAX_EVICTION_MARKS {
637 let Some((_, oldest)) = self.order.pop_first() else {
638 break;
639 };
640 if let Some(mark) = self.by_file.remove(&oldest) {
641 self.overflowed.insert(mark.owner);
642 }
643 }
644 }
645
646 fn forget_file(&mut self, file: &DiagnosticsKey) {
647 if let Some(mark) = self.by_file.remove(file) {
648 self.order.remove(&mark.seq);
649 }
650 }
651
652 fn forget_server(&mut self, server: &ServerId) {
653 self.by_file.retain(|_, mark| &mark.owner != server);
654 self.order.retain(|_, file| self.by_file.contains_key(file));
655 self.overflowed.remove(server);
656 }
657
658 fn content(&self, file: &DiagnosticsKey) -> Option<EvictedContent> {
659 self.by_file.get(file).map(|mark| mark.content)
660 }
661
662 fn has_overflowed(&self, server: &ServerId) -> bool {
663 self.overflowed.contains(server)
664 }
665
666 fn clear_overflow(&mut self, server: &ServerId) -> bool {
669 self.overflowed.remove(server)
670 }
671
672 fn restore_overflow(&mut self, server: &ServerId) {
673 self.overflowed.insert(server.clone());
674 }
675}
676
677pub fn message_as_str(message: &lsp_types::Message) -> &str {
681 match message {
682 lsp_types::Message::String(s) => s,
683 lsp_types::Message::MarkupContent(m) => &m.value,
684 }
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize)]
689pub struct DiagnosticInfo {
690 pub uri: Uri,
692 pub version: Option<DocumentVersion>,
694 pub diagnostics: Vec<LspDiagnostic>,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq)]
700enum SourceOrigin {
701 Pushed(Spelling),
703 Pulled,
705}
706
707impl SourceOrigin {
708 const fn is_pulled(&self) -> bool {
709 matches!(self, Self::Pulled)
710 }
711}
712
713impl From<&Origin> for SourceOrigin {
714 fn from(origin: &Origin) -> Self {
715 match origin {
716 Origin::Pushed { spelling, .. } => Self::Pushed(spelling.clone()),
717 Origin::Pulled { .. } => Self::Pulled,
718 }
719 }
720}
721
722#[derive(Debug, Clone)]
724struct SourceEntry {
725 info: DiagnosticInfo,
726 origin: SourceOrigin,
727 freshness: Freshness,
728}
729
730#[derive(Debug, Clone)]
738pub struct DiagnosticSources {
739 requested: Uri,
740 entries: Vec<SourceEntry>,
741}
742
743impl DiagnosticSources {
744 pub(crate) fn with_pulled(
747 mut self,
748 file: &Uri,
749 version: Option<DocumentVersion>,
750 diagnostics: BoundedDiagnostics,
751 ) -> Self {
752 self.entries.retain(|entry| !entry.origin.is_pulled());
753 self.entries.push(SourceEntry {
754 info: DiagnosticInfo {
755 uri: file.clone(),
756 version,
757 diagnostics: diagnostics.0,
758 },
759 origin: SourceOrigin::Pulled,
760 freshness: Freshness::Current,
761 });
762 self
763 }
764
765 #[must_use]
791 pub fn merge(self) -> Option<DiagnosticInfo> {
792 let Self { requested, entries } = self;
793 let has_pulled = entries.iter().any(|entry| entry.origin.is_pulled());
794 if entries.len() <= 1 && !has_pulled {
795 return entries.into_iter().next().map(|entry| entry.info);
796 }
797 let version = entries
798 .iter()
799 .find(|entry| {
800 entry.origin == SourceOrigin::Pushed(Spelling::Canonical)
801 && entry.freshness == Freshness::Current
802 })
803 .and_then(|entry| entry.info.version)
804 .or_else(|| {
805 entries
806 .iter()
807 .find(|entry| entry.origin.is_pulled())
808 .and_then(|entry| entry.info.version)
809 });
810
811 let (pulled, pushed): (Vec<SourceEntry>, Vec<SourceEntry>) = entries
812 .into_iter()
813 .partition(|entry| entry.origin.is_pulled());
814 let pulled: Vec<LspDiagnostic> = pulled
815 .into_iter()
816 .flat_map(|entry| entry.info.diagnostics)
817 .collect();
818 let index = PulledIndex::new(&pulled);
819 let mut seen: HashSet<Vec<u8>> = HashSet::new();
820 let mut kept_pushed = Vec::new();
821 let pushed_items = pushed.into_iter().flat_map(|entry| {
822 let SourceEntry {
823 info, freshness, ..
824 } = entry;
825 info.diagnostics
826 .into_iter()
827 .enumerate()
828 .filter(move |(position, _)| !freshness.excludes(*position))
829 .map(|(_, diagnostic)| diagnostic)
830 });
831 for diagnostic in pushed_items {
832 if index.contains_same_problem(&diagnostic) {
833 continue;
834 }
835 if serde_json::to_vec(&diagnostic).map_or(true, |bytes| seen.insert(bytes)) {
838 kept_pushed.push(diagnostic);
839 }
840 }
841 drop(index);
842 let mut merged = pulled;
843 merged.extend(kept_pushed);
844 merged.sort_by_key(|d| (point(d.range.start), point(d.range.end)));
845 cap_diagnostics_entry_size(&requested, &mut merged);
846
847 Some(DiagnosticInfo {
848 uri: requested,
849 version,
850 diagnostics: merged,
851 })
852 }
853}
854
855#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
857pub struct LogEntry {
858 pub level: LogLevel,
860 pub message: String,
862 pub timestamp: DateTime<Utc>,
864}
865
866#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
868#[serde(rename_all = "lowercase")]
869#[schemars(inline)]
870pub enum LogLevel {
871 Error,
873 Warning,
875 Info,
877 Debug,
879}
880
881impl LogLevel {
882 const fn rank(self) -> u8 {
883 match self {
884 Self::Error => 0,
885 Self::Warning => 1,
886 Self::Info => 2,
887 Self::Debug => 3,
888 }
889 }
890
891 #[must_use]
903 pub const fn meets(self, min: Self) -> bool {
904 self.rank() <= min.rank()
905 }
906}
907
908impl From<lsp_types::MessageType> for LogLevel {
909 fn from(msg_type: lsp_types::MessageType) -> Self {
910 match msg_type {
911 lsp_types::MessageType::Error => Self::Error,
912 lsp_types::MessageType::Warning => Self::Warning,
913 lsp_types::MessageType::Info => Self::Info,
914 _ => Self::Debug,
916 }
917 }
918}
919
920#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
922pub struct ServerMessage {
923 pub message_type: MessageType,
925 pub message: String,
927 pub timestamp: DateTime<Utc>,
929}
930
931#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
933#[serde(rename_all = "lowercase")]
934pub enum MessageType {
935 Error,
937 Warning,
939 Info,
941 Log,
943}
944
945impl From<lsp_types::MessageType> for MessageType {
946 fn from(msg_type: lsp_types::MessageType) -> Self {
947 match msg_type {
948 lsp_types::MessageType::Error => Self::Error,
949 lsp_types::MessageType::Warning => Self::Warning,
950 lsp_types::MessageType::Info => Self::Info,
951 _ => Self::Log,
953 }
954 }
955}
956
957#[derive(Debug)]
959pub struct NotificationCache {
960 entries: HashMap<SlotKey, CachedEntry>,
962 order: HashMap<ServerId, BTreeMap<u64, SlotKey>>,
974 files: HashMap<DiagnosticsKey, BTreeSet<SlotKey>>,
977 next_diagnostic_seq: u64,
982 next_pull_ticket: u64,
984 pull_sources: HashMap<ServerId, Vec<DiagnosticSource>>,
988 clear_epochs: HashMap<ServerId, ClearEpoch>,
991 diagnostics_route_count: Option<usize>,
1006
1007 empty_diagnostics_count: usize,
1017 recent_evictions: EvictionRecord,
1024 eviction_marks: EvictionMarks,
1028 documents: Option<Arc<DocumentTracker>>,
1031 logs: VecDeque<LogEntry>,
1033 messages: VecDeque<ServerMessage>,
1035 push_degraded: HashSet<ServerId>,
1046 indexing: IndexingTracker,
1052}
1053
1054impl Default for NotificationCache {
1055 fn default() -> Self {
1056 Self::new()
1057 }
1058}
1059
1060impl NotificationCache {
1061 #[must_use]
1063 pub fn new() -> Self {
1064 Self {
1065 entries: HashMap::with_capacity(32),
1066 order: HashMap::new(),
1067 files: HashMap::new(),
1068 next_diagnostic_seq: 0,
1069 next_pull_ticket: 0,
1070 pull_sources: HashMap::new(),
1071 clear_epochs: HashMap::new(),
1072 diagnostics_route_count: None,
1073 empty_diagnostics_count: 0,
1074 recent_evictions: EvictionRecord::default(),
1075 eviction_marks: EvictionMarks::default(),
1076 documents: None,
1077 logs: VecDeque::with_capacity(MAX_LOG_ENTRIES),
1078 messages: VecDeque::with_capacity(MAX_SERVER_MESSAGES),
1079 push_degraded: HashSet::new(),
1080 indexing: IndexingTracker::new(),
1081 }
1082 }
1083
1084 pub(crate) fn set_diagnostics_route_count(&mut self, count: usize) {
1101 self.diagnostics_route_count = Some(count.max(1));
1102 }
1103
1104 #[cfg(test)]
1105 pub(crate) const fn configured_route_count(&self) -> Option<usize> {
1106 self.diagnostics_route_count
1107 }
1108
1109 fn per_server_budget(&self) -> usize {
1128 let count = self
1129 .diagnostics_route_count
1130 .unwrap_or_else(|| {
1131 self.order
1132 .values()
1133 .filter(|order| !order.is_empty())
1134 .count()
1135 })
1136 .max(1);
1137 MAX_DIAGNOSTIC_ENTRIES
1138 .checked_div(count)
1139 .unwrap_or(1)
1140 .max(1)
1141 }
1142
1143 fn server_to_evict_from(&self, writer: &ServerId) -> Option<ServerId> {
1167 let largest = self
1168 .order
1169 .iter()
1170 .filter(|(_, order)| !order.is_empty())
1171 .max_by_key(|(id, order)| (order.len(), id.as_str()));
1172
1173 let budget = self.per_server_budget();
1174 if let Some((id, order)) = largest
1175 && order.len() > budget
1176 {
1177 return Some(id.clone());
1178 }
1179
1180 if self
1181 .order
1182 .get(writer)
1183 .is_some_and(|order| !order.is_empty())
1184 {
1185 return Some(writer.clone());
1186 }
1187
1188 largest.map(|(id, _)| id.clone())
1189 }
1190
1191 fn is_empty_entry(&self, key: &SlotKey) -> bool {
1197 self.entries
1198 .get(key)
1199 .is_some_and(|entry| entry.diagnostics.is_empty())
1200 }
1201
1202 fn is_protected(&self, key: &SlotKey, protect: Protect<'_>) -> bool {
1204 match protect {
1205 Protect::Nothing => false,
1206 Protect::File(file) => self
1207 .entries
1208 .get(key)
1209 .is_some_and(|entry| entry.file(key) == file),
1210 }
1211 }
1212
1213 fn oldest_empty_entry_in(
1216 &self,
1217 server: &ServerId,
1218 protect: Protect<'_>,
1219 ) -> Option<(u64, SlotKey)> {
1220 let order = self.order.get(server)?;
1221 order
1222 .iter()
1223 .find(|(_, key)| self.is_empty_entry(key) && !self.is_protected(key, protect))
1224 .map(|(&seq, key)| (seq, key.clone()))
1225 }
1226
1227 fn oldest_entry_in(&self, server: &ServerId, protect: Protect<'_>) -> Option<(u64, SlotKey)> {
1229 self.order
1230 .get(server)?
1231 .iter()
1232 .find(|(_, key)| !self.is_protected(key, protect))
1233 .map(|(&seq, key)| (seq, key.clone()))
1234 }
1235
1236 fn entry_to_evict(
1272 &self,
1273 writer: &ServerId,
1274 protect: Protect<'_>,
1275 ) -> Option<(ServerId, u64, SlotKey)> {
1276 let evict_from = self.server_to_evict_from(writer)?;
1277
1278 if self.empty_diagnostics_count > 0 {
1279 if let Some((seq, key)) = self.oldest_empty_entry_in(&evict_from, protect) {
1280 return Some((evict_from, seq, key));
1281 }
1282
1283 let budget = self.per_server_budget();
1284 let cross_server_pick = self
1285 .order
1286 .iter()
1287 .filter(|(id, order)| order.len() > budget && *id != &evict_from)
1288 .filter_map(|(id, order)| {
1289 self.oldest_empty_entry_in(id, protect)
1290 .map(|(seq, key)| (id, order.len(), seq, key))
1291 })
1292 .max_by_key(|(id, len, ..)| (*len, id.as_str()));
1293
1294 if let Some((id, _, seq, key)) = cross_server_pick {
1295 return Some((id.clone(), seq, key));
1296 }
1297 }
1298
1299 if let Some((seq, key)) = self.oldest_entry_in(&evict_from, protect) {
1300 return Some((evict_from, seq, key));
1301 }
1302 self.order
1303 .iter()
1304 .filter(|(id, _)| *id != &evict_from)
1305 .filter_map(|(id, order)| {
1306 self.oldest_entry_in(id, protect)
1307 .map(|(seq, key)| (id, order.len(), seq, key))
1308 })
1309 .max_by_key(|(id, len, ..)| (*len, id.as_str()))
1310 .map(|(id, _, seq, key)| (id.clone(), seq, key))
1311 }
1312
1313 pub(crate) fn write_published_diagnostics(
1354 &mut self,
1355 server_id: &ServerId,
1356 published: &PublishedDiagnosticsUri,
1357 version: Option<DocumentVersion>,
1358 diagnostics: BoundedDiagnostics,
1359 ) -> PushWrite {
1360 let source_key = SlotKey::pushed(DiagnosticsKey::of(published.source()));
1361 let canonical_key = DiagnosticsKey::of(published.canonical());
1362
1363 let already_indexed = self
1364 .entries
1365 .get(&source_key)
1366 .is_some_and(|entry| entry.file(&source_key) == &canonical_key);
1367 let at_capacity = self.pushed_slot_count(&canonical_key) >= MAX_SOURCES_PER_FILE;
1368 if !already_indexed && at_capacity {
1369 let alias_to_evict = published
1370 .is_canonical()
1371 .then(|| self.oldest_alias_source(&canonical_key))
1372 .flatten();
1373 let Some(oldest_alias) = alias_to_evict else {
1374 debug!(
1375 "dropping diagnostics for {}: {MAX_SOURCES_PER_FILE} published URIs already cached for {}",
1376 published.source().as_ref(),
1377 published.canonical().as_ref()
1378 );
1379 return PushWrite::Dropped {
1380 evicted: Vec::new(),
1381 };
1382 };
1383 self.evict_entry(&oldest_alias);
1384 }
1385
1386 let diagnostics = diagnostics.into_vec();
1387
1388 self.drop_superseded_pull(&canonical_key, published.canonical(), version);
1389
1390 drop(self.take_entry(&source_key));
1394 let evicted = match self.make_room(server_id, Protect::Nothing) {
1395 Ok(evicted) => evicted,
1396 Err(NoRoom { evicted }) => {
1397 debug!(
1398 "dropping diagnostics for {}: no entry could be evicted to make room",
1399 published.source().as_ref()
1400 );
1401 return PushWrite::Dropped { evicted };
1402 }
1403 };
1404
1405 let seq = self.next_seq();
1406 let opening = self.document_opening(published.canonical());
1407 let spelling = if published.is_canonical() {
1408 Spelling::Canonical
1409 } else {
1410 Spelling::Alias(canonical_key)
1411 };
1412 self.insert_entry(
1413 source_key,
1414 CachedEntry {
1415 uri: published.source().clone(),
1416 diagnostics,
1417 owner: server_id.clone(),
1418 seq,
1419 origin: Origin::Pushed {
1420 spelling,
1421 version,
1422 opening,
1423 covered: Covered::default(),
1424 },
1425 },
1426 );
1427 PushWrite::Stored { evicted }
1428 }
1429
1430 #[cfg(test)]
1433 pub(crate) fn store_published_diagnostics(
1434 &mut self,
1435 server_id: &ServerId,
1436 published: &PublishedDiagnosticsUri,
1437 version: Option<i32>,
1438 diagnostics: Vec<LspDiagnostic>,
1439 ) {
1440 drop(self.write_published_diagnostics(
1441 server_id,
1442 published,
1443 version.map(DocumentVersion::new),
1444 BoundedDiagnostics::new(published.source(), diagnostics),
1445 ));
1446 }
1447
1448 const fn next_seq(&mut self) -> u64 {
1449 let seq = self.next_diagnostic_seq;
1450 self.next_diagnostic_seq = self.next_diagnostic_seq.saturating_add(1);
1451 seq
1452 }
1453
1454 fn pushed_slot_count(&self, file: &DiagnosticsKey) -> usize {
1456 self.files.get(file).map_or(0, |slots| {
1457 slots
1458 .iter()
1459 .filter(|slot| slot.provenance == Provenance::Pushed)
1460 .count()
1461 })
1462 }
1463
1464 fn drop_superseded_pull(
1471 &mut self,
1472 file: &DiagnosticsKey,
1473 canonical: &Uri,
1474 pushed_version: Option<DocumentVersion>,
1475 ) {
1476 let slot = SlotKey::pulled(file.clone());
1477 let Some(entry) = self.entries.get(&slot) else {
1478 return;
1479 };
1480 let Origin::Pulled {
1481 opening: pulled_opening,
1482 version: pulled,
1483 ..
1484 } = entry.origin
1485 else {
1486 return;
1487 };
1488 let owner = entry.owner.clone();
1489 let sync = self.document_sync(&owner, canonical);
1490 let reopened =
1491 self.documents.is_some() && pulled_opening != self.document_opening(canonical);
1492 if reopened || pull_is_superseded(pulled, pushed_version, sync) {
1493 debug!(
1494 "dropping the pulled diagnostics of {}: superseded by a push \
1495 (pulled {pulled:?}, pushed {pushed_version:?}, {sync:?})",
1496 canonical.as_ref()
1497 );
1498 drop(self.take_entry(&slot));
1499 }
1500 }
1501
1502 pub(crate) fn attach_documents(&mut self, documents: Arc<DocumentTracker>) {
1506 self.documents = Some(documents);
1507 }
1508
1509 fn document_sync(&self, owner: &ServerId, canonical: &Uri) -> DocumentSync {
1512 let Some(documents) = &self.documents else {
1513 return DocumentSync::Unattached;
1514 };
1515 uri_to_path(canonical)
1516 .and_then(|path| documents.synced_version(&path, owner))
1517 .map_or(DocumentSync::NotOpen, DocumentSync::Synced)
1518 }
1519
1520 fn document_opening(&self, canonical: &Uri) -> Option<Opening> {
1523 let path = uri_to_path(canonical)?;
1524 self.documents.as_ref()?.opening(&path)
1525 }
1526
1527 pub(crate) fn begin_pull(&mut self, server: &ServerId, version: DocumentVersion) -> PullStamp {
1533 let ticket = PullTicket(self.next_pull_ticket);
1534 self.next_pull_ticket = self.next_pull_ticket.saturating_add(1);
1535 PullStamp {
1536 ticket,
1537 epoch: self.clear_epochs.get(server).copied().unwrap_or_default(),
1538 version,
1539 }
1540 }
1541
1542 #[cfg(test)]
1554 pub(crate) fn store_pulled_diagnostics(
1555 &mut self,
1556 server_id: &ServerId,
1557 file: &Uri,
1558 stamp: PullStamp,
1559 check: VersionCheck,
1560 items: BoundedDiagnostics,
1561 ) -> PullWrite {
1562 self.store_pulled_diagnostics_with(
1563 server_id,
1564 file,
1565 stamp,
1566 check,
1567 items,
1568 SnapshotBefore::Take,
1569 )
1570 }
1571
1572 pub(crate) fn store_pulled_diagnostics_with(
1576 &mut self,
1577 server_id: &ServerId,
1578 file: &Uri,
1579 stamp: PullStamp,
1580 check: VersionCheck,
1581 items: BoundedDiagnostics,
1582 before: SnapshotBefore,
1583 ) -> PullWrite {
1584 let discard = |reason, items| PullWrite::Discarded {
1585 reason,
1586 evicted: Vec::new(),
1587 items,
1588 };
1589 if stamp.epoch
1590 != self
1591 .clear_epochs
1592 .get(server_id)
1593 .copied()
1594 .unwrap_or_default()
1595 {
1596 return discard(Discard::ServerCleared, items);
1597 }
1598 if check == VersionCheck::Moved {
1599 return discard(Discard::VersionMoved, items);
1600 }
1601 let file_key = DiagnosticsKey::of(file);
1602 let slot_key = SlotKey::pulled(file_key.clone());
1603 if self.entries.get(&slot_key).is_some_and(|stored| {
1604 matches!(stored.origin, Origin::Pulled { ticket, .. } if ticket >= stamp.ticket)
1605 }) {
1606 return discard(Discard::OlderTicket, items);
1607 }
1608
1609 let opening = self.document_opening(file);
1610 let same_state = self.entries.get(&slot_key).is_some_and(|entry| {
1611 entry.diagnostics == items.0
1612 && matches!(
1613 entry.origin,
1614 Origin::Pulled { opening: stored, version, .. }
1615 if stored == opening && version == stamp.version
1616 )
1617 });
1618 let gained = self.coverage_gained(&slot_key);
1619 let slot = if same_state && gained.is_empty() {
1620 SlotChange::Identical
1621 } else {
1622 SlotChange::Replaced {
1623 before: (before == SnapshotBefore::Take).then(|| self.diagnostic_sources(file)),
1624 }
1625 };
1626
1627 drop(self.take_entry_with_coverage(&slot_key, gained));
1628 let evicted = match self.make_room(server_id, Protect::File(&file_key)) {
1629 Ok(evicted) => evicted,
1630 Err(NoRoom { evicted }) => {
1631 return PullWrite::Discarded {
1632 reason: Discard::NoRoom,
1633 evicted,
1634 items,
1635 };
1636 }
1637 };
1638
1639 if !items.0.is_empty() {
1640 self.learn_pull_sources(server_id, &items.0);
1641 }
1642 let seq = self.next_seq();
1643 self.insert_entry(
1644 slot_key,
1645 CachedEntry {
1646 uri: file.clone(),
1647 diagnostics: items.0,
1648 owner: server_id.clone(),
1649 seq,
1650 origin: Origin::Pulled {
1651 ticket: stamp.ticket,
1652 opening,
1653 version: stamp.version,
1654 },
1655 },
1656 );
1657 PullWrite::Stored { slot, evicted }
1658 }
1659
1660 #[cfg(test)]
1663 pub(crate) fn store_pulled_for_test(
1664 &mut self,
1665 server_id: &ServerId,
1666 file: &Uri,
1667 diagnostics: Vec<LspDiagnostic>,
1668 ) {
1669 let stamp = self.begin_pull(server_id, DocumentVersion::FIRST);
1670 drop(self.store_pulled_diagnostics(
1671 server_id,
1672 file,
1673 stamp,
1674 VersionCheck::Current,
1675 BoundedDiagnostics::new(file, diagnostics),
1676 ));
1677 }
1678
1679 #[cfg(test)]
1681 pub(crate) fn store_diagnostics(
1682 &mut self,
1683 server_id: &ServerId,
1684 uri: &Uri,
1685 version: Option<i32>,
1686 diagnostics: Vec<LspDiagnostic>,
1687 ) {
1688 let published = PublishedDiagnosticsUri::for_test(uri.clone(), uri.clone());
1689 self.store_published_diagnostics(server_id, &published, version, diagnostics);
1690 }
1691
1692 fn insert_entry(&mut self, key: SlotKey, entry: CachedEntry) {
1696 debug_assert!(!self.entries.contains_key(&key));
1697 debug_assert_eq!(key.provenance, entry.origin.provenance());
1698 self.eviction_marks.forget_file(entry.file(&key));
1699 self.order
1700 .entry(entry.owner.clone())
1701 .or_default()
1702 .insert(entry.seq, key.clone());
1703 self.files
1704 .entry(entry.file(&key).clone())
1705 .or_default()
1706 .insert(key.clone());
1707 if entry.diagnostics.is_empty() {
1708 self.empty_diagnostics_count = self.empty_diagnostics_count.saturating_add(1);
1709 }
1710 self.entries.insert(key, entry);
1711 }
1712
1713 fn take_entry(&mut self, key: &SlotKey) -> Option<CachedEntry> {
1720 let gained = if key.provenance == Provenance::Pulled {
1721 self.coverage_gained(key)
1722 } else {
1723 Vec::new()
1724 };
1725 self.take_entry_with_coverage(key, gained)
1726 }
1727
1728 fn take_entry_with_coverage(
1731 &mut self,
1732 key: &SlotKey,
1733 gained: Vec<(SlotKey, BTreeSet<usize>)>,
1734 ) -> Option<CachedEntry> {
1735 for (pushed, gained) in gained {
1736 if let Some(CachedEntry {
1737 origin: Origin::Pushed { covered, .. },
1738 ..
1739 }) = self.entries.get_mut(&pushed)
1740 {
1741 covered.0.extend(gained);
1742 }
1743 }
1744 let entry = self.entries.remove(key)?;
1745 if let Some(order) = self.order.get_mut(&entry.owner) {
1746 order.remove(&entry.seq);
1747 }
1748 let file = entry.file(key);
1749 if let Some(members) = self.files.get_mut(file) {
1750 members.remove(key);
1751 if members.is_empty() {
1752 self.files.remove(file);
1753 }
1754 }
1755 if entry.diagnostics.is_empty() {
1756 self.empty_diagnostics_count = self.empty_diagnostics_count.saturating_sub(1);
1757 }
1758 Some(entry)
1759 }
1760
1761 fn coverage_gained(&self, slot: &SlotKey) -> Vec<(SlotKey, BTreeSet<usize>)> {
1766 let Some(pulled) = self.entries.get(slot) else {
1767 return Vec::new();
1768 };
1769 let Origin::Pulled {
1770 opening, version, ..
1771 } = &pulled.origin
1772 else {
1773 return Vec::new();
1774 };
1775 let Some(members) = self.files.get(&slot.uri) else {
1776 return Vec::new();
1777 };
1778 let index = PulledIndex::new(&pulled.diagnostics);
1779 members
1780 .iter()
1781 .filter(|key| key.provenance == Provenance::Pushed)
1782 .filter_map(|key| {
1783 let entry = self.entries.get(key)?;
1784 let Origin::Pushed {
1785 opening: pushed_opening,
1786 covered,
1787 ..
1788 } = &entry.origin
1789 else {
1790 return None;
1791 };
1792 let same_state = entry.owner == pulled.owner
1793 && entry.origin.version() == Some(*version)
1794 && pushed_opening.is_none_or(|pushed| Some(pushed) == *opening);
1795 if !same_state {
1796 return None;
1797 }
1798 let gained: BTreeSet<usize> = entry
1799 .diagnostics
1800 .iter()
1801 .enumerate()
1802 .filter(|(position, diagnostic)| {
1803 !covered.0.contains(position) && index.contains_same_problem(diagnostic)
1804 })
1805 .map(|(position, _)| position)
1806 .collect();
1807 (!gained.is_empty()).then(|| (key.clone(), gained))
1808 })
1809 .collect()
1810 }
1811
1812 fn learn_pull_sources(&mut self, owner: &ServerId, items: &[LspDiagnostic]) {
1815 let learned = self.pull_sources.entry(owner.clone()).or_default();
1816 for source in items.iter().filter_map(DiagnosticSource::of) {
1817 if learned.len() >= MAX_LEARNED_PULL_SOURCES {
1818 break;
1819 }
1820 if !learned.contains(&source) {
1821 learned.push(source);
1822 }
1823 }
1824 }
1825
1826 fn push_freshness(
1838 &self,
1839 key: &DiagnosticsKey,
1840 requested: &Uri,
1841 entry: &CachedEntry,
1842 ) -> Freshness {
1843 let Origin::Pushed {
1844 opening, covered, ..
1845 } = &entry.origin
1846 else {
1847 return Freshness::Current;
1848 };
1849 let Some(version) = entry.origin.version() else {
1850 return Freshness::Current;
1851 };
1852 let DocumentSync::Synced(synced) = self.document_sync(&entry.owner, requested) else {
1853 return Freshness::Current;
1854 };
1855 let current_opening = self.document_opening(requested);
1856 let same_opening = opening.is_none_or(|opening| Some(opening) == current_opening);
1857 if same_opening && version == synced {
1858 return Freshness::Current;
1859 }
1860 let answered_at_synced_state =
1861 self.entries
1862 .get(&SlotKey::pulled(key.clone()))
1863 .is_some_and(|pulled| {
1864 pulled.owner == entry.owner
1865 && matches!(
1866 pulled.origin,
1867 Origin::Pulled { opening: pulled_opening, version: pulled_version, .. }
1868 if pulled_version == synced && pulled_opening == current_opening
1869 )
1870 });
1871 if !answered_at_synced_state {
1872 return Freshness::Current;
1873 }
1874 let mut covered = covered.clone();
1875 if same_opening && let Some(learned) = self.pull_sources.get(&entry.owner) {
1876 covered.0.extend(
1877 entry
1878 .diagnostics
1879 .iter()
1880 .enumerate()
1881 .filter(|(_, diagnostic)| {
1882 DiagnosticSource::of(diagnostic)
1883 .is_some_and(|source| learned.contains(&source))
1884 })
1885 .map(|(position, _)| position),
1886 );
1887 }
1888 Freshness::Superseded { covered }
1889 }
1890
1891 fn make_room(
1899 &mut self,
1900 writer: &ServerId,
1901 protect: Protect<'_>,
1902 ) -> Result<Vec<DiagnosticsKey>, NoRoom> {
1903 let was_overflowed = self.eviction_marks.clear_overflow(writer);
1904 let mut evicted = Vec::new();
1905 while self.entries.len() >= MAX_DIAGNOSTIC_ENTRIES {
1906 let Some((_, _, victim)) = self.entry_to_evict(writer, protect) else {
1907 if was_overflowed {
1908 self.eviction_marks.restore_overflow(writer);
1909 }
1910 return Err(NoRoom { evicted });
1911 };
1912 evicted.extend(self.evict_for_capacity(&victim));
1913 }
1914 Ok(evicted)
1915 }
1916
1917 fn evict_entry(&mut self, key: &SlotKey) {
1921 let Some(entry) = self.take_entry(key) else {
1922 return;
1923 };
1924 if entry.diagnostics.is_empty() {
1925 self.record_empty_eviction(entry.file(key).clone(), std::time::Instant::now());
1926 }
1927 }
1928
1929 fn evict_for_capacity(&mut self, key: &SlotKey) -> Option<DiagnosticsKey> {
1935 let entry = self.entries.get(key)?;
1936 let file = entry.file(key).clone();
1937 let owner = entry.owner.clone();
1938 let content = if entry.diagnostics.is_empty() {
1939 EvictedContent::Clean
1940 } else {
1941 EvictedContent::Lost
1942 };
1943 self.evict_entry(key);
1944 if content == EvictedContent::Lost || !self.files.contains_key(&file) {
1945 self.eviction_marks.record(file.clone(), owner, content);
1946 }
1947 Some(file)
1948 }
1949
1950 fn record_empty_eviction(&mut self, file: DiagnosticsKey, now: std::time::Instant) {
1951 self.recent_evictions.record(file, now);
1952 }
1953
1954 fn was_recently_evicted(&self, uri: &Uri, now: std::time::Instant) -> bool {
1957 let key = DiagnosticsKey::of(uri);
1958 !self.has_diagnostics(uri) && self.recent_evictions.contains(&key, now)
1959 }
1960
1961 pub(crate) fn is_listen_replayable(&self, uri: &Uri) -> bool {
1966 self.has_diagnostics(uri) || self.was_recently_evicted(uri, std::time::Instant::now())
1967 }
1968
1969 #[cfg(test)]
1972 fn assert_consistent(&self) {
1973 let mut ordered = 0;
1974 for (server, order) in &self.order {
1975 for (seq, key) in order {
1976 let entry = self
1977 .entries
1978 .get(key)
1979 .unwrap_or_else(|| panic!("ordered key has no entry"));
1980 assert_eq!((&entry.owner, entry.seq), (server, *seq));
1981 ordered += 1;
1982 }
1983 }
1984 assert_eq!(ordered, self.entries.len(), "order and entries diverge");
1985
1986 let mut filed = 0;
1987 for (file, members) in &self.files {
1988 assert!(!members.is_empty(), "empty file set kept");
1989 for key in members {
1990 let entry = self
1991 .entries
1992 .get(key)
1993 .unwrap_or_else(|| panic!("filed key has no entry"));
1994 assert_eq!(entry.file(key), file);
1995 filed += 1;
1996 }
1997 }
1998 assert_eq!(filed, self.entries.len(), "files and entries diverge");
1999
2000 let empty = self
2001 .entries
2002 .values()
2003 .filter(|entry| entry.diagnostics.is_empty())
2004 .count();
2005 assert_eq!(empty, self.empty_diagnostics_count);
2006
2007 let marks = &self.eviction_marks;
2008 assert_eq!(marks.by_file.len(), marks.order.len(), "marks diverge");
2009 for (seq, file) in &marks.order {
2010 assert_eq!(marks.by_file.get(file).map(|mark| mark.seq), Some(*seq));
2011 }
2012 }
2013
2014 fn oldest_alias_source(&self, canonical_key: &DiagnosticsKey) -> Option<SlotKey> {
2017 self.files
2018 .get(canonical_key)?
2019 .iter()
2020 .filter_map(|source| {
2021 let entry = self.entries.get(source)?;
2022 matches!(
2023 entry.origin,
2024 Origin::Pushed {
2025 spelling: Spelling::Alias(_),
2026 ..
2027 }
2028 )
2029 .then_some((entry.seq, source))
2030 })
2031 .min_by_key(|(seq, _)| *seq)
2032 .map(|(_, source)| source.clone())
2033 }
2034
2035 fn source_keys(&self, key: &DiagnosticsKey) -> BTreeSet<SlotKey> {
2038 let mut keys: BTreeSet<SlotKey> =
2039 self.files.get(key).into_iter().flatten().cloned().collect();
2040 let direct = SlotKey::pushed(key.clone());
2041 if self.entries.contains_key(&direct) {
2042 keys.insert(direct);
2043 }
2044 keys
2045 }
2046
2047 pub(crate) fn store_log(&mut self, level: LogLevel, message: String) {
2052 let entry = LogEntry {
2053 level,
2054 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
2055 timestamp: Utc::now(),
2056 };
2057
2058 if self.logs.len() >= MAX_LOG_ENTRIES {
2059 self.logs.pop_front();
2060 }
2061 self.logs.push_back(entry);
2062 }
2063
2064 pub(crate) fn store_message(&mut self, message_type: MessageType, message: String) {
2069 let msg = ServerMessage {
2070 message_type,
2071 message: truncate_string(message, MAX_ENTRY_TEXT_BYTES),
2072 timestamp: Utc::now(),
2073 };
2074
2075 if self.messages.len() >= MAX_SERVER_MESSAGES {
2076 self.messages.pop_front();
2077 }
2078 self.messages.push_back(msg);
2079 }
2080
2081 pub(crate) fn observe_indexing_signal(
2100 &mut self,
2101 server_id: &ServerId,
2102 method: &str,
2103 params: Option<&serde_json::Value>,
2104 ) {
2105 self.indexing
2106 .observe_server_status(server_id, method, params);
2107 }
2108
2109 pub(crate) fn observe_progress(
2116 &mut self,
2117 server_id: &ServerId,
2118 params: &lsp_types::ProgressParams,
2119 ) {
2120 self.indexing.observe_progress(server_id, params);
2121 }
2122
2123 pub(crate) fn set_indexing_policy(&mut self, server_id: ServerId, policy: IndexingPolicy) {
2126 self.indexing.set_policy(server_id, policy);
2127 }
2128
2129 #[must_use]
2142 pub fn indexing_state(&self, server_id: &ServerId) -> IndexingState {
2143 self.indexing.state(server_id)
2144 }
2145
2146 pub(crate) fn reset_indexing_state(&mut self, server_id: &ServerId, reset: IndexingReset) {
2159 self.indexing.reset(server_id, reset);
2160 }
2161
2162 #[inline]
2175 #[must_use]
2176 pub fn diagnostics(&self, uri: &Uri) -> Option<DiagnosticInfo> {
2177 self.entries
2178 .get(&SlotKey::pushed(DiagnosticsKey::of(uri)))
2179 .map(CachedEntry::info)
2180 }
2181
2182 #[must_use]
2201 pub fn diagnostic_sources(&self, uri: &Uri) -> DiagnosticSources {
2202 let key = DiagnosticsKey::of(uri);
2203 let entries = self
2204 .source_keys(&key)
2205 .into_iter()
2206 .filter_map(|source| {
2207 let entry = self.entries.get(&source)?;
2208 Some(SourceEntry {
2209 info: entry.info(),
2210 origin: SourceOrigin::from(&entry.origin),
2211 freshness: self.push_freshness(&key, uri, entry),
2212 })
2213 })
2214 .collect();
2215 DiagnosticSources {
2216 requested: uri.clone(),
2217 entries,
2218 }
2219 }
2220
2221 #[must_use]
2235 pub fn has_diagnostics(&self, uri: &Uri) -> bool {
2236 let key = DiagnosticsKey::of(uri);
2237 self.entries.contains_key(&SlotKey::pushed(key.clone())) || self.files.contains_key(&key)
2238 }
2239
2240 #[must_use]
2261 pub fn availability(&self, uri: &Uri, route: Option<&ServerId>) -> DiagnosticsAvailability {
2262 if self.has_diagnostics(uri) {
2263 return DiagnosticsAvailability::Published;
2264 }
2265 match self.eviction_marks.content(&DiagnosticsKey::of(uri)) {
2266 Some(EvictedContent::Clean) => DiagnosticsAvailability::Published,
2267 Some(EvictedContent::Lost) => DiagnosticsAvailability::Evicted,
2268 None if route.is_some_and(|server| self.eviction_marks.has_overflowed(server)) => {
2269 DiagnosticsAvailability::Evicted
2270 }
2271 None => DiagnosticsAvailability::Pending,
2272 }
2273 }
2274
2275 #[inline]
2280 #[must_use]
2281 pub fn diagnostics_owner(&self, uri: &Uri) -> Option<&ServerId> {
2282 let key = DiagnosticsKey::of(uri);
2283 self.entries
2284 .get(&SlotKey::pushed(key.clone()))
2285 .or_else(|| {
2286 let slots = self.files.get(&key)?;
2287 slots
2288 .iter()
2289 .filter(|slot| slot.provenance == Provenance::Pushed)
2290 .chain(slots.iter())
2291 .find_map(|source| self.entries.get(source))
2292 })
2293 .map(|entry| &entry.owner)
2294 }
2295
2296 #[inline]
2298 #[must_use]
2299 pub const fn logs(&self) -> &VecDeque<LogEntry> {
2300 &self.logs
2301 }
2302
2303 #[inline]
2305 #[must_use]
2306 pub const fn messages(&self) -> &VecDeque<ServerMessage> {
2307 &self.messages
2308 }
2309
2310 pub(crate) fn clear_server_diagnostics(&mut self, server_id: &ServerId) -> Vec<DiagnosticsKey> {
2320 let epoch = self.clear_epochs.entry(server_id.clone()).or_default();
2321 epoch.0 = epoch.0.saturating_add(1);
2322 self.eviction_marks.forget_server(server_id);
2323 self.pull_sources.remove(server_id);
2324 let Some(order) = self.order.remove(server_id) else {
2325 return Vec::new();
2326 };
2327 let mut cleared = Vec::with_capacity(order.len());
2328 for key in order.into_values() {
2329 if let Some(entry) = self.entries.get(&key) {
2330 cleared.push(entry.file(&key).clone());
2331 }
2332 self.evict_entry(&key);
2333 }
2334 cleared.sort_unstable();
2335 cleared.dedup();
2336 cleared
2337 }
2338
2339 pub(crate) fn record_lost_files(
2348 &mut self,
2349 owner: &ServerId,
2350 files: &[DiagnosticsKey],
2351 ) -> Vec<DiagnosticsKey> {
2352 for file in files {
2353 let stale: Vec<SlotKey> = self
2354 .files
2355 .get(file)
2356 .into_iter()
2357 .flatten()
2358 .filter(|key| {
2359 key.provenance == Provenance::Pushed
2360 && self
2361 .entries
2362 .get(key)
2363 .is_some_and(|entry| entry.owner == *owner)
2364 })
2365 .cloned()
2366 .collect();
2367 for key in stale {
2368 self.evict_entry(&key);
2369 }
2370 if !self.has_diagnostics_for(file) {
2371 self.eviction_marks
2372 .record(file.clone(), owner.clone(), EvictedContent::Lost);
2373 }
2374 }
2375 files.to_vec()
2376 }
2377
2378 pub(crate) fn record_unnamed_loss(&mut self, owner: &ServerId) {
2381 self.eviction_marks.restore_overflow(owner);
2382 }
2383
2384 fn has_diagnostics_for(&self, key: &DiagnosticsKey) -> bool {
2386 self.files.contains_key(key) || self.entries.contains_key(&SlotKey::pushed(key.clone()))
2387 }
2388
2389 pub(crate) fn mark_push_degraded(&mut self, server_id: &ServerId) {
2393 self.push_degraded.insert(server_id.clone());
2394 }
2395
2396 pub(crate) fn clear_push_degraded(&mut self, server_id: &ServerId) {
2399 self.push_degraded.remove(server_id);
2400 }
2401
2402 #[inline]
2408 #[must_use]
2409 pub(crate) fn is_push_degraded(&self, server_id: &ServerId) -> bool {
2410 self.push_degraded.contains(server_id)
2411 }
2412
2413 #[inline]
2415 #[must_use]
2416 #[cfg(test)]
2417 pub(crate) fn diagnostics_count(&self) -> usize {
2418 self.entries.len()
2419 }
2420
2421 #[inline]
2423 #[must_use]
2424 #[cfg(test)]
2425 pub(crate) fn logs_count(&self) -> usize {
2426 self.logs.len()
2427 }
2428
2429 #[inline]
2431 #[must_use]
2432 #[cfg(test)]
2433 pub(crate) fn messages_count(&self) -> usize {
2434 self.messages.len()
2435 }
2436}
2437
2438pub fn apply_lifecycle_notification(
2475 cache: &mut NotificationCache,
2476 server_id: &ServerId,
2477 notif: crate::lsp::LspNotification,
2478) {
2479 match notif {
2480 crate::lsp::LspNotification::Progress(params) => {
2481 cache.observe_progress(server_id, ¶ms);
2482 }
2483 crate::lsp::LspNotification::Other { method, params } => {
2484 cache.observe_indexing_signal(server_id, &method, params.as_ref());
2485 }
2486 crate::lsp::LspNotification::PublishDiagnostics(_)
2487 | crate::lsp::LspNotification::LogMessage(_)
2488 | crate::lsp::LspNotification::ShowMessage(_) => {}
2489 }
2490}
2491
2492pub async fn on_lifecycle(
2498 cache: Option<&tokio::sync::Mutex<NotificationCache>>,
2499 server_id: &ServerId,
2500 pinned_tsserver: Option<&std::path::Path>,
2501 notif: crate::lsp::LspNotification,
2502) {
2503 crate::lsp::tsserver_pin::warn_if_pin_ignored(pinned_tsserver, ¬if, server_id);
2504 if let Some(cache) = cache {
2505 apply_lifecycle_notification(&mut *cache.lock().await, server_id, notif);
2506 }
2507}
2508
2509#[cfg(test)]
2510mod pull_tests;
2511
2512#[cfg(test)]
2513mod tests {
2514 use lsp_types::{Position, Range};
2515
2516 use super::bounds::*;
2517 use super::*;
2518 use crate::bridge::indexing::IndexingReset;
2519 use crate::test_lsp::CapturedLogs;
2520
2521 fn test_server() -> ServerId {
2525 ServerId::from_static("test-server")
2526 }
2527
2528 #[test]
2529 fn test_notification_cache_new() {
2530 let cache = NotificationCache::new();
2531 assert_eq!(cache.diagnostics_count(), 0);
2532 assert_eq!(cache.logs_count(), 0);
2533 assert_eq!(cache.messages_count(), 0);
2534 }
2535
2536 #[test]
2537 fn test_store_and_diagnostics() {
2538 let mut cache = NotificationCache::new();
2539 let uri: Uri = Uri::from("file:///test.rs");
2540
2541 let diagnostic = LspDiagnostic {
2542 range: Range {
2543 start: Position {
2544 line: 0,
2545 character: 0,
2546 },
2547 end: Position {
2548 line: 0,
2549 character: 5,
2550 },
2551 },
2552 severity: Some(lsp_types::DiagnosticSeverity::Error),
2553 message: "test error".to_string().into(),
2554 code: None,
2555 source: None,
2556 code_description: None,
2557 related_information: None,
2558 tags: None,
2559 data: None,
2560 };
2561
2562 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2563
2564 let stored = cache.diagnostics(&uri).unwrap();
2565 assert_eq!(stored.uri, uri);
2566 assert_eq!(stored.version.map(DocumentVersion::get), Some(1));
2567 assert_eq!(stored.diagnostics.len(), 1);
2568 assert_eq!(
2569 stored.diagnostics[0].message,
2570 lsp_types::Message::String("test error".to_string())
2571 );
2572 }
2573
2574 #[test]
2577 fn test_store_diagnostics_truncates_oversized_message() {
2578 let mut cache = NotificationCache::new();
2579 let uri: Uri = Uri::from("file:///test.rs");
2580 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
2581
2582 let diagnostic = LspDiagnostic {
2583 range: Range {
2584 start: Position {
2585 line: 0,
2586 character: 0,
2587 },
2588 end: Position {
2589 line: 0,
2590 character: 5,
2591 },
2592 },
2593 severity: Some(lsp_types::DiagnosticSeverity::Error),
2594 message: oversized.clone().into(),
2595 code: None,
2596 source: None,
2597 code_description: None,
2598 related_information: None,
2599 tags: None,
2600 data: None,
2601 };
2602
2603 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2604
2605 let stored = cache.diagnostics(&uri).unwrap();
2606 let stored = message_as_str(&stored.diagnostics[0].message);
2607 assert!(stored.len() < oversized.len());
2608 assert!(stored.ends_with("... (truncated)"));
2609 }
2610
2611 fn minimal_diagnostic(message: String) -> LspDiagnostic {
2614 LspDiagnostic {
2615 range: Range {
2616 start: Position {
2617 line: 0,
2618 character: 0,
2619 },
2620 end: Position {
2621 line: 0,
2622 character: 5,
2623 },
2624 },
2625 severity: Some(lsp_types::DiagnosticSeverity::Error),
2626 message: message.into(),
2627 code: None,
2628 source: None,
2629 code_description: None,
2630 related_information: None,
2631 tags: None,
2632 data: None,
2633 }
2634 }
2635
2636 #[test]
2639 fn test_reported_severity_orders_most_severe_first_and_defaults_to_information() {
2640 let mut severityless = minimal_diagnostic("none".to_string());
2641 severityless.severity = None;
2642 let mut hint = minimal_diagnostic("hint".to_string());
2643 hint.severity = Some(lsp_types::DiagnosticSeverity::Hint);
2644
2645 assert_eq!(
2646 ReportedSeverity::of(&severityless),
2647 ReportedSeverity::Information
2648 );
2649 assert!(ReportedSeverity::of(&severityless) < ReportedSeverity::of(&hint));
2650 assert!(ReportedSeverity::Error < ReportedSeverity::Warning);
2651 assert!(ReportedSeverity::Warning < ReportedSeverity::Information);
2652 }
2653
2654 #[test]
2658 fn test_store_diagnostics_caps_aggregate_size_for_many_small_diagnostics() {
2659 let mut cache = NotificationCache::new();
2660 let uri: Uri = Uri::from("file:///test.rs");
2661
2662 let diagnostics: Vec<LspDiagnostic> = (0..5000)
2666 .map(|i| {
2667 minimal_diagnostic(format!(
2668 "diagnostic number {i}, padded: {}",
2669 "x".repeat(200)
2670 ))
2671 })
2672 .collect();
2673 let original_count = diagnostics.len();
2674
2675 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2676
2677 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2678 assert!(
2679 stored.len() < original_count,
2680 "aggregate cap must trim the list, kept {} of {original_count}",
2681 stored.len()
2682 );
2683 assert!(!stored.is_empty(), "must keep at least one diagnostic");
2684 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2685 assert!(
2686 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2687 "stored entry must fit the aggregate cap, got {serialized_len} bytes"
2688 );
2689 }
2690
2691 #[test]
2695 fn test_store_diagnostics_truncation_keeps_largest_fitting_prefix() {
2696 let mut cache = NotificationCache::new();
2697 let uri: Uri = Uri::from("file:///test.rs");
2698
2699 let diagnostics: Vec<LspDiagnostic> = (0..5000)
2703 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
2704 .collect();
2705
2706 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2707
2708 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2709 assert!(
2710 stored.len() > 2600,
2711 "largest-fitting-prefix search must keep far more than half, kept {}",
2712 stored.len()
2713 );
2714 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2715 assert!(serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES);
2716 let mut with_one_more = stored.clone();
2720 with_one_more.push(minimal_diagnostic(format!(
2721 "diagnostic overflow: {}",
2722 "x".repeat(250)
2723 )));
2724 assert!(
2725 serde_json::to_vec(&with_one_more).unwrap().len() > MAX_DIAGNOSTICS_ENTRY_BYTES,
2726 "kept count must be the largest that fits, not merely a fitting count"
2727 );
2728 }
2729
2730 #[test]
2734 fn test_store_diagnostics_truncation_prefers_higher_severity() {
2735 let mut cache = NotificationCache::new();
2736 let uri: Uri = Uri::from("file:///test.rs");
2737
2738 let mut diagnostics: Vec<LspDiagnostic> = (0..5000)
2739 .map(|i| {
2740 let mut d = minimal_diagnostic(format!("hint {i}: {}", "x".repeat(200)));
2741 d.severity = Some(lsp_types::DiagnosticSeverity::Hint);
2742 d
2743 })
2744 .collect();
2745 let mut trailing_error = minimal_diagnostic("the one real error".to_string());
2746 trailing_error.severity = Some(lsp_types::DiagnosticSeverity::Error);
2747 diagnostics.push(trailing_error);
2748
2749 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2750
2751 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2752 assert!(
2753 stored
2754 .iter()
2755 .any(|d| message_as_str(&d.message) == "the one real error"),
2756 "the trailing ERROR diagnostic must survive truncation over leading HINT noise"
2757 );
2758 }
2759
2760 #[test]
2764 fn test_store_diagnostics_warns_when_truncating_list() {
2765 use tracing_subscriber::layer::SubscriberExt as _;
2766
2767 let mut cache = NotificationCache::new();
2768 let uri: Uri = Uri::from("file:///test.rs");
2769 let diagnostics: Vec<LspDiagnostic> = (0..5000)
2770 .map(|i| minimal_diagnostic(format!("diagnostic {i}: {}", "x".repeat(250))))
2771 .collect();
2772
2773 let captured = CapturedLogs::default();
2774 let subscriber = tracing_subscriber::registry().with(captured.clone());
2775 let guard = tracing::subscriber::set_default(subscriber);
2776 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2777 drop(guard);
2778
2779 let messages = captured.messages();
2780 assert!(
2781 messages
2782 .iter()
2783 .any(|m| m.contains("highest-severity") && m.contains("file:///test.rs")),
2784 "expected a truncation warning naming the URI, got: {messages:?}"
2785 );
2786 }
2787
2788 #[test]
2792 fn test_store_diagnostics_warns_when_dropping_data_blob() {
2793 use tracing_subscriber::layer::SubscriberExt as _;
2794
2795 let mut cache = NotificationCache::new();
2796 let uri: Uri = Uri::from("file:///test.rs");
2797 let mut diagnostic = minimal_diagnostic("small message".to_string());
2798 diagnostic.data = Some(serde_json::json!({
2799 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
2800 }));
2801
2802 let captured = CapturedLogs::default();
2803 let subscriber = tracing_subscriber::registry().with(captured.clone());
2804 let guard = tracing::subscriber::set_default(subscriber);
2805 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2806 drop(guard);
2807
2808 let messages = captured.messages();
2809 assert!(
2810 messages.iter().any(|m| m.contains("code-action")),
2811 "expected a warning noting the code-action quick-fix impact, got: {messages:?}"
2812 );
2813 }
2814
2815 #[test]
2820 fn test_store_diagnostics_drops_oversized_data_blob_on_single_diagnostic() {
2821 let mut cache = NotificationCache::new();
2822 let uri: Uri = Uri::from("file:///test.rs");
2823
2824 let mut diagnostic = minimal_diagnostic("small message".to_string());
2825 diagnostic.data = Some(serde_json::json!({
2826 "blob": "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
2827 }));
2828
2829 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2830
2831 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2832 assert_eq!(stored.len(), 1);
2833 assert_eq!(
2834 stored[0].message,
2835 lsp_types::Message::String("small message".to_string())
2836 );
2837 assert!(
2838 stored[0].data.is_none(),
2839 "oversized data blob must be dropped"
2840 );
2841 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2842 assert!(
2843 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2844 "stored entry must fit the aggregate cap after dropping data, got {serialized_len} bytes"
2845 );
2846 }
2847
2848 #[test]
2853 fn test_store_diagnostics_truncates_oversized_source_on_single_diagnostic() {
2854 let mut cache = NotificationCache::new();
2855 let uri: Uri = Uri::from("file:///test.rs");
2856
2857 let mut diagnostic = minimal_diagnostic("small message".to_string());
2858 diagnostic.source = Some("x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000));
2859
2860 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2861
2862 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2863 assert_eq!(stored.len(), 1);
2864 assert_eq!(
2865 stored[0].message,
2866 lsp_types::Message::String("small message".to_string())
2867 );
2868 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2869 assert!(
2870 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2871 "stored entry must fit the aggregate cap after truncating source, got {serialized_len} bytes"
2872 );
2873 }
2874
2875 #[test]
2882 fn test_store_diagnostics_caps_single_diagnostic_with_every_field_maxed_out() {
2883 let mut cache = NotificationCache::new();
2884 let uri: Uri = Uri::from("file:///test.rs");
2885
2886 let mut diagnostic = minimal_diagnostic("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
2891 diagnostic.source = Some("x".repeat(MAX_ENTRY_TEXT_BYTES + 1000));
2892 diagnostic.code = Some(lsp_types::Code::String(
2893 "x".repeat(MAX_ENTRY_TEXT_BYTES + 1000),
2894 ));
2895 diagnostic.data = Some(serde_json::json!({ "blob": "x".repeat(MAX_ENTRY_TEXT_BYTES) }));
2896 diagnostic.tags = Some(vec![lsp_types::DiagnosticTag::Unnecessary; 50]);
2897 diagnostic.related_information = Some(vec![
2898 lsp_types::DiagnosticRelatedInformation {
2899 location: lsp_types::Location {
2900 uri: uri.clone(),
2901 range: Range::default(),
2902 },
2903 message: "x".repeat(1000),
2904 };
2905 5
2906 ]);
2907
2908 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2909
2910 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2911 assert_eq!(stored.len(), 1);
2912 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2913 assert!(
2914 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2915 "postcondition must hold even with every field maxed out, got {serialized_len} bytes"
2916 );
2917 }
2918
2919 #[test]
2928 fn test_cap_diagnostics_entry_size_terminal_fallback_bounds_untruncated_message() {
2929 let uri: Uri = Uri::from("file:///test.rs");
2930 let mut diagnostics = vec![minimal_diagnostic(
2931 "x".repeat(MAX_DIAGNOSTICS_ENTRY_BYTES + 1000),
2932 )];
2933
2934 cap_diagnostics_entry_size(&uri, &mut diagnostics);
2935
2936 assert_eq!(diagnostics.len(), 1);
2937 assert!(
2938 message_as_str(&diagnostics[0].message).len()
2939 <= DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES + 20,
2940 "terminal fallback must truncate the message itself, got {} bytes",
2941 message_as_str(&diagnostics[0].message).len()
2942 );
2943 let serialized_len = serde_json::to_vec(&diagnostics).unwrap().len();
2944 assert!(
2945 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2946 "postcondition must hold via the terminal fallback, got {serialized_len} bytes"
2947 );
2948 }
2949
2950 #[test]
2955 fn test_store_diagnostics_cheap_path_leaves_small_diagnostics_untouched() {
2956 let mut cache = NotificationCache::new();
2957 let uri: Uri = Uri::from("file:///test.rs");
2958
2959 let mut diagnostic = minimal_diagnostic("a small, ordinary diagnostic message".to_string());
2960 diagnostic.source = Some("rustc".to_string());
2961
2962 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
2963
2964 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2965 assert_eq!(stored.len(), 1);
2966 assert_eq!(
2967 stored[0].message,
2968 lsp_types::Message::String("a small, ordinary diagnostic message".to_string())
2969 );
2970 assert_eq!(stored[0].source.as_deref(), Some("rustc"));
2971 }
2972
2973 #[test]
2983 fn test_store_diagnostics_cheap_path_escape_safe_for_control_character_heavy_message() {
2984 let mut cache = NotificationCache::new();
2985 let uri: Uri = Uri::from("file:///test.rs");
2986
2987 let nul_heavy_message = "\0".repeat(MAX_ENTRY_TEXT_BYTES);
2988 let diagnostics: Vec<LspDiagnostic> = (0..3)
2989 .map(|_| minimal_diagnostic(nul_heavy_message.clone()))
2990 .collect();
2991
2992 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
2993
2994 let stored = &cache.diagnostics(&uri).unwrap().diagnostics;
2995 let serialized_len = serde_json::to_vec(stored).unwrap().len();
2996 assert!(
2997 serialized_len <= MAX_DIAGNOSTICS_ENTRY_BYTES,
2998 "escape-heavy content must not let the cheap-estimate fast path skip the real cap, \
2999 got {serialized_len} bytes"
3000 );
3001 }
3002
3003 #[test]
3004 fn test_store_diagnostics_replaces_existing() {
3005 let mut cache = NotificationCache::new();
3006 let uri: Uri = Uri::from("file:///test.rs");
3007
3008 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3009 assert_eq!(cache.diagnostics_count(), 1);
3010
3011 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
3012 assert_eq!(cache.diagnostics_count(), 1);
3013
3014 let stored = cache.diagnostics(&uri).unwrap();
3015 assert_eq!(stored.version.map(DocumentVersion::get), Some(2));
3016 }
3017
3018 #[test]
3019 fn test_store_and_get_logs() {
3020 let mut cache = NotificationCache::new();
3021
3022 cache.store_log(LogLevel::Error, "error message".to_string());
3023 cache.store_log(LogLevel::Info, "info message".to_string());
3024
3025 let logs = cache.logs();
3026 assert_eq!(logs.len(), 2);
3027 assert_eq!(logs[0].level, LogLevel::Error);
3028 assert_eq!(logs[0].message, "error message");
3029 assert_eq!(logs[1].level, LogLevel::Info);
3030 assert_eq!(logs[1].message, "info message");
3031 }
3032
3033 #[test]
3034 fn test_logs_max_capacity() {
3035 let mut cache = NotificationCache::new();
3036
3037 for i in 0..MAX_LOG_ENTRIES + 10 {
3039 cache.store_log(LogLevel::Info, format!("message {i}"));
3040 }
3041
3042 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
3043
3044 let logs = cache.logs();
3046 assert_eq!(logs.front().unwrap().message, "message 10");
3047 assert_eq!(
3048 logs.back().unwrap().message,
3049 format!("message {}", MAX_LOG_ENTRIES + 9)
3050 );
3051 }
3052
3053 #[test]
3057 fn test_store_log_truncates_oversized_message() {
3058 let mut cache = NotificationCache::new();
3059 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
3060
3061 cache.store_log(LogLevel::Info, oversized.clone());
3062
3063 let stored = &cache.logs()[0].message;
3064 assert!(stored.len() < oversized.len());
3065 assert!(stored.ends_with("... (truncated)"));
3066 }
3067
3068 #[test]
3069 fn test_store_log_does_not_truncate_message_at_or_below_limit() {
3070 let mut cache = NotificationCache::new();
3071 let message = "a".repeat(MAX_ENTRY_TEXT_BYTES);
3072
3073 cache.store_log(LogLevel::Info, message.clone());
3074
3075 assert_eq!(cache.logs()[0].message, message);
3076 }
3077
3078 #[test]
3079 fn test_store_and_get_messages() {
3080 let mut cache = NotificationCache::new();
3081
3082 cache.store_message(MessageType::Error, "error msg".to_string());
3083 cache.store_message(MessageType::Warning, "warning msg".to_string());
3084
3085 let messages = cache.messages();
3086 assert_eq!(messages.len(), 2);
3087 assert_eq!(messages[0].message_type, MessageType::Error);
3088 assert_eq!(messages[0].message, "error msg");
3089 assert_eq!(messages[1].message_type, MessageType::Warning);
3090 assert_eq!(messages[1].message, "warning msg");
3091 }
3092
3093 #[test]
3094 fn test_messages_max_capacity() {
3095 let mut cache = NotificationCache::new();
3096
3097 for i in 0..MAX_SERVER_MESSAGES + 10 {
3099 cache.store_message(MessageType::Info, format!("message {i}"));
3100 }
3101
3102 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
3103
3104 let messages = cache.messages();
3106 assert_eq!(messages.front().unwrap().message, "message 10");
3107 assert_eq!(
3108 messages.back().unwrap().message,
3109 format!("message {}", MAX_SERVER_MESSAGES + 9)
3110 );
3111 }
3112
3113 #[test]
3115 fn test_store_message_truncates_oversized_message() {
3116 let mut cache = NotificationCache::new();
3117 let oversized = "a".repeat(MAX_ENTRY_TEXT_BYTES + 100);
3118
3119 cache.store_message(MessageType::Info, oversized.clone());
3120
3121 let stored = &cache.messages()[0].message;
3122 assert!(stored.len() < oversized.len());
3123 assert!(stored.ends_with("... (truncated)"));
3124 }
3125
3126 #[test]
3127 fn test_log_levels() {
3128 let mut cache = NotificationCache::new();
3129
3130 cache.store_log(LogLevel::Error, "error".to_string());
3131 cache.store_log(LogLevel::Warning, "warning".to_string());
3132 cache.store_log(LogLevel::Info, "info".to_string());
3133 cache.store_log(LogLevel::Debug, "debug".to_string());
3134
3135 let logs = cache.logs();
3136 assert_eq!(logs[0].level, LogLevel::Error);
3137 assert_eq!(logs[1].level, LogLevel::Warning);
3138 assert_eq!(logs[2].level, LogLevel::Info);
3139 assert_eq!(logs[3].level, LogLevel::Debug);
3140 }
3141
3142 #[test]
3143 fn test_message_types() {
3144 let mut cache = NotificationCache::new();
3145
3146 cache.store_message(MessageType::Error, "error".to_string());
3147 cache.store_message(MessageType::Warning, "warning".to_string());
3148 cache.store_message(MessageType::Info, "info".to_string());
3149 cache.store_message(MessageType::Log, "log".to_string());
3150
3151 let messages = cache.messages();
3152 assert_eq!(messages[0].message_type, MessageType::Error);
3153 assert_eq!(messages[1].message_type, MessageType::Warning);
3154 assert_eq!(messages[2].message_type, MessageType::Info);
3155 assert_eq!(messages[3].message_type, MessageType::Log);
3156 }
3157
3158 #[test]
3159 fn test_timestamp_ordering() {
3160 let mut cache = NotificationCache::new();
3161
3162 cache.store_log(LogLevel::Info, "first".to_string());
3163 std::thread::sleep(std::time::Duration::from_millis(10));
3164 cache.store_log(LogLevel::Info, "second".to_string());
3165
3166 let logs = cache.logs();
3167 assert!(logs[0].timestamp < logs[1].timestamp);
3168 }
3169
3170 #[test]
3171 fn test_store_diagnostics_empty_list() {
3172 let mut cache = NotificationCache::new();
3173 let uri: Uri = Uri::from("file:///test.rs");
3174
3175 let diagnostic = LspDiagnostic {
3176 range: Range {
3177 start: Position {
3178 line: 0,
3179 character: 0,
3180 },
3181 end: Position {
3182 line: 0,
3183 character: 5,
3184 },
3185 },
3186 severity: Some(lsp_types::DiagnosticSeverity::Error),
3187 message: "test error".to_string().into(),
3188 code: None,
3189 source: None,
3190 code_description: None,
3191 related_information: None,
3192 tags: None,
3193 data: None,
3194 };
3195
3196 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![diagnostic]);
3197 assert_eq!(cache.diagnostics(&uri).unwrap().diagnostics.len(), 1);
3198
3199 cache.store_diagnostics(&test_server(), &uri, Some(2), vec![]);
3200 let stored = cache.diagnostics(&uri).unwrap();
3201 assert_eq!(stored.diagnostics.len(), 0);
3202 assert_eq!(stored.version.map(DocumentVersion::get), Some(2));
3203 }
3204
3205 #[test]
3206 fn test_store_many_diagnostics_single_file() {
3207 let mut cache = NotificationCache::new();
3208 let uri: Uri = Uri::from("file:///test.rs");
3209
3210 let diagnostics: Vec<LspDiagnostic> = (0..100)
3211 .map(|i| LspDiagnostic {
3212 range: Range {
3213 start: Position {
3214 line: i,
3215 character: 0,
3216 },
3217 end: Position {
3218 line: i,
3219 character: 10,
3220 },
3221 },
3222 message: format!("Error {i}").into(),
3223 severity: Some(lsp_types::DiagnosticSeverity::Error),
3224 code: None,
3225 source: None,
3226 code_description: None,
3227 related_information: None,
3228 tags: None,
3229 data: None,
3230 })
3231 .collect();
3232
3233 cache.store_diagnostics(&test_server(), &uri, Some(1), diagnostics);
3234
3235 let stored = cache.diagnostics(&uri).unwrap();
3236 assert_eq!(stored.diagnostics.len(), 100);
3237 }
3238
3239 #[test]
3240 fn test_logs_exact_capacity_boundary() {
3241 let mut cache = NotificationCache::new();
3242
3243 for i in 0..MAX_LOG_ENTRIES {
3244 cache.store_log(LogLevel::Info, format!("message {i}"));
3245 }
3246 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
3247
3248 cache.store_log(LogLevel::Info, "overflow".to_string());
3249 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
3250 assert_eq!(cache.logs().front().unwrap().message, "message 1");
3251 }
3252
3253 #[test]
3254 fn test_messages_exact_capacity_boundary() {
3255 let mut cache = NotificationCache::new();
3256
3257 for i in 0..MAX_SERVER_MESSAGES {
3258 cache.store_message(MessageType::Info, format!("message {i}"));
3259 }
3260 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
3261
3262 cache.store_message(MessageType::Info, "overflow".to_string());
3263 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
3264 assert_eq!(cache.messages().front().unwrap().message, "message 1");
3265 }
3266
3267 #[test]
3268 fn test_diagnostics_max_capacity() {
3269 let mut cache = NotificationCache::new();
3270
3271 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 10 {
3272 let uri: Uri = Uri::from(format!("file:///test{i}.rs"));
3273 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3274 }
3275
3276 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3277
3278 let evicted: Uri = Uri::from("file:///test0.rs");
3280 assert!(cache.diagnostics(&evicted).is_none());
3281 let newest: Uri = Uri::from(format!("file:///test{}.rs", MAX_DIAGNOSTIC_ENTRIES + 9));
3282 assert!(cache.diagnostics(&newest).is_some());
3283 }
3284
3285 #[test]
3286 fn test_diagnostics_replacing_existing_uri_does_not_trigger_eviction() {
3287 let mut cache = NotificationCache::new();
3288 let uri: Uri = Uri::from("file:///stable.rs");
3289
3290 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
3291 cache.store_diagnostics(
3292 &test_server(),
3293 &uri,
3294 Some(i32::try_from(i).unwrap()),
3295 vec![],
3296 );
3297 }
3298 assert_eq!(cache.diagnostics_count(), 1);
3299 assert!(cache.diagnostics(&uri).is_some());
3300 }
3301
3302 #[test]
3303 fn test_diagnostics_republish_refreshes_eviction_order() {
3304 let mut cache = NotificationCache::new();
3308 let actively_edited: Uri = Uri::from("file:///keep.rs");
3309 cache.store_diagnostics(&test_server(), &actively_edited, Some(1), vec![]);
3310
3311 for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
3313 let uri: Uri = Uri::from(format!("file:///untouched{i}.rs"));
3314 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3315 }
3316 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3317
3318 cache.store_diagnostics(&test_server(), &actively_edited, Some(2), vec![]);
3322
3323 let overflow: Uri = Uri::from("file:///overflow.rs");
3326 cache.store_diagnostics(&test_server(), &overflow, Some(1), vec![]);
3327
3328 assert!(
3329 cache.diagnostics(&actively_edited).is_some(),
3330 "republished entry must survive eviction after being refreshed"
3331 );
3332 let oldest_untouched: Uri = Uri::from("file:///untouched0.rs");
3333 assert!(
3334 cache.diagnostics(&oldest_untouched).is_none(),
3335 "the oldest never-republished entry must be evicted instead"
3336 );
3337 assert!(cache.diagnostics(&overflow).is_some());
3338 }
3339
3340 #[test]
3341 fn test_store_diagnostics_no_version() {
3342 let mut cache = NotificationCache::new();
3343 let uri: Uri = Uri::from("file:///test.rs");
3344
3345 cache.store_diagnostics(&test_server(), &uri, None, vec![]);
3346 let stored = cache.diagnostics(&uri).unwrap();
3347 assert_eq!(stored.version, None);
3348 }
3349
3350 #[test]
3356 fn test_noisy_server_does_not_evict_quiet_server_entries() {
3357 let mut cache = NotificationCache::new();
3358 cache.set_diagnostics_route_count(2);
3359 let noisy = ServerId::from_static("noisy");
3360 let quiet = ServerId::from_static("quiet");
3361
3362 let quiet_uri: Uri = Uri::from("file:///quiet/only_file.rs");
3363 cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
3364
3365 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
3370 let uri: Uri = Uri::from(format!("file:///noisy/file{i}.rs"));
3371 cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
3372 }
3373
3374 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3375 assert!(
3376 cache.diagnostics(&quiet_uri).is_some(),
3377 "quiet server's only entry must survive the noisy server's overflow"
3378 );
3379
3380 let noisy_first: Uri = Uri::from("file:///noisy/file0.rs");
3381 assert!(
3382 cache.diagnostics(&noisy_first).is_none(),
3383 "noisy server's own oldest entries must be evicted once the aggregate cache is full"
3384 );
3385 }
3386
3387 #[test]
3393 fn test_dominant_server_exceeds_equal_share_while_others_idle() {
3394 let mut cache = NotificationCache::new();
3395 cache.set_diagnostics_route_count(4);
3396 let dominant = ServerId::from_static("dominant");
3397
3398 let equal_share = MAX_DIAGNOSTIC_ENTRIES / 4;
3399 let more_than_share = equal_share + 100;
3400 for i in 0..more_than_share {
3401 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3402 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
3403 }
3404 assert_eq!(
3405 cache.diagnostics_count(),
3406 more_than_share,
3407 "a dominant server must be able to exceed its static equal share while the aggregate has room"
3408 );
3409
3410 for i in more_than_share..MAX_DIAGNOSTIC_ENTRIES {
3413 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3414 cache.store_diagnostics(&dominant, &uri, Some(1), vec![]);
3415 }
3416 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3417 }
3418
3419 #[test]
3432 fn test_eviction_target_tie_break_is_deterministic() {
3433 let mut cache = NotificationCache::new();
3434 cache.set_diagnostics_route_count(1000); let a = ServerId::from_static("a");
3437 let b = ServerId::from_static("b");
3438 for i in 0..2 {
3439 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
3440 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
3441 }
3442 for i in 0..2 {
3443 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
3444 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
3445 }
3446
3447 let writer = ServerId::from_static("writer");
3451 assert_eq!(cache.server_to_evict_from(&writer), Some(b));
3452 }
3453
3454 #[test]
3463 fn test_new_writer_still_evicts_when_every_existing_server_is_in_share() {
3464 let mut cache = NotificationCache::new();
3465 cache.set_diagnostics_route_count(2); let a = ServerId::from_static("a");
3468 let b = ServerId::from_static("b");
3469 for i in 0..500 {
3470 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
3471 cache.store_diagnostics(&a, &uri, Some(1), vec![]);
3472 }
3473 for i in 0..500 {
3474 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
3475 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
3476 }
3477 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3478
3479 let c = ServerId::from_static("c");
3482 let new_uri: Uri = Uri::from("file:///c/first.rs");
3483 cache.store_diagnostics(&c, &new_uri, Some(1), vec![]);
3484
3485 assert_eq!(
3486 cache.diagnostics_count(),
3487 MAX_DIAGNOSTIC_ENTRIES,
3488 "the aggregate cap must still be enforced even when every existing server is within share"
3489 );
3490 assert!(cache.diagnostics(&new_uri).is_some());
3491
3492 let b_oldest: Uri = Uri::from("file:///b/file0.rs");
3496 assert!(
3497 cache.diagnostics(&b_oldest).is_none(),
3498 "the largest in-share server (tie-broken to b) must lose its oldest entry"
3499 );
3500 assert!(
3501 cache
3502 .diagnostics(&Uri::from("file:///a/file0.rs".to_owned()))
3503 .is_some(),
3504 "the other in-share server must be untouched"
3505 );
3506 }
3507
3508 #[test]
3511 fn test_repeated_writes_same_owner_do_not_grow_order_map() {
3512 let mut cache = NotificationCache::new();
3513 let server = ServerId::from_static("server");
3514 let uri: Uri = Uri::from("file:///test.rs");
3515
3516 let max_version = i32::try_from(MAX_DIAGNOSTIC_ENTRIES).unwrap() + 10;
3517 for version in 0..max_version {
3518 cache.store_diagnostics(&server, &uri, Some(version), vec![]);
3519 }
3520
3521 assert_eq!(cache.diagnostics_count(), 1);
3522 let stored = cache.diagnostics(&uri).unwrap();
3523 assert_eq!(
3524 stored.version.map(DocumentVersion::get),
3525 Some(max_version - 1)
3526 );
3527 }
3528
3529 #[test]
3533 fn test_store_diagnostics_reassigns_ownership() {
3534 let mut cache = NotificationCache::new();
3535 let old_owner = ServerId::from_static("old");
3536 let new_owner = ServerId::from_static("new");
3537 let uri: Uri = Uri::from("file:///test.rs");
3538
3539 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
3540 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
3541
3542 assert_eq!(cache.diagnostics_count(), 1);
3543 let stored = cache.diagnostics(&uri).unwrap();
3544 assert_eq!(stored.version.map(DocumentVersion::get), Some(2));
3545
3546 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
3550 let other: Uri = Uri::from(format!("file:///old/file{i}.rs"));
3551 cache.store_diagnostics(&old_owner, &other, Some(1), vec![]);
3552 }
3553 assert!(cache.diagnostics(&uri).is_some());
3554 }
3555
3556 #[test]
3561 fn test_diagnostics_owner_returns_publisher_after_store() {
3562 let mut cache = NotificationCache::new();
3563 let server = ServerId::from_static("rust");
3564 let uri: Uri = Uri::from("file:///main.rs");
3565
3566 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
3567
3568 assert_eq!(cache.diagnostics_owner(&uri), Some(&server));
3569 }
3570
3571 #[test]
3572 fn test_diagnostics_owner_none_for_untracked_uri() {
3573 let cache = NotificationCache::new();
3574 let uri: Uri = Uri::from("file:///never-seen.rs");
3575
3576 assert_eq!(cache.diagnostics_owner(&uri), None);
3577 }
3578
3579 #[test]
3584 fn test_diagnostics_owner_reflects_reassigned_ownership() {
3585 let mut cache = NotificationCache::new();
3586 let old_owner = ServerId::from_static("old");
3587 let new_owner = ServerId::from_static("new");
3588 let uri: Uri = Uri::from("file:///test.rs");
3589
3590 cache.store_diagnostics(&old_owner, &uri, Some(1), vec![]);
3591 assert_eq!(cache.diagnostics_owner(&uri), Some(&old_owner));
3592
3593 cache.store_diagnostics(&new_owner, &uri, Some(2), vec![]);
3594 assert_eq!(cache.diagnostics_owner(&uri), Some(&new_owner));
3595 }
3596
3597 #[test]
3600 fn test_clear_server_diagnostics_scopes_to_one_server() {
3601 let mut cache = NotificationCache::new();
3602 let crashed = ServerId::from_static("crashed");
3603 let healthy = ServerId::from_static("healthy");
3604
3605 let crashed_uri: Uri = Uri::from("file:///crashed/main.py");
3606 let healthy_uri: Uri = Uri::from("file:///healthy/main.rs");
3607 cache.store_diagnostics(&crashed, &crashed_uri, Some(1), vec![]);
3608 cache.store_diagnostics(&healthy, &healthy_uri, Some(1), vec![]);
3609
3610 cache.clear_server_diagnostics(&crashed);
3611
3612 assert!(cache.diagnostics(&crashed_uri).is_none());
3613 assert!(cache.diagnostics(&healthy_uri).is_some());
3614 assert_eq!(cache.diagnostics_count(), 1);
3615
3616 cache.clear_server_diagnostics(&crashed);
3618 assert_eq!(cache.diagnostics_count(), 1);
3619 }
3620
3621 #[test]
3626 fn test_push_degraded_is_scoped_per_server_and_permanent() {
3627 let mut cache = NotificationCache::new();
3628 let degraded = ServerId::from_static("degraded");
3629 let healthy = ServerId::from_static("healthy");
3630
3631 assert!(!cache.is_push_degraded(°raded));
3632 assert!(!cache.is_push_degraded(&healthy));
3633
3634 cache.mark_push_degraded(°raded);
3635
3636 assert!(cache.is_push_degraded(°raded));
3637 assert!(!cache.is_push_degraded(&healthy));
3638
3639 cache.mark_push_degraded(°raded);
3641 assert!(cache.is_push_degraded(°raded));
3642 }
3643
3644 #[test]
3652 fn test_shrinking_budget_affects_eviction_target_not_existing_entries() {
3653 let mut cache = NotificationCache::new();
3654 let server = ServerId::from_static("server");
3655
3656 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
3657 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3658 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
3659 }
3660 assert_eq!(
3661 cache.diagnostics_count(),
3662 MAX_DIAGNOSTIC_ENTRIES,
3663 "filling to the aggregate cap must not evict anything early"
3664 );
3665
3666 cache.set_diagnostics_route_count(4);
3669 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3670
3671 let other = ServerId::from_static("other");
3674 let new_uri: Uri = Uri::from("file:///other/new.rs");
3675 cache.store_diagnostics(&other, &new_uri, Some(1), vec![]);
3676
3677 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3678 assert!(cache.diagnostics(&new_uri).is_some());
3679 let server_oldest: Uri = Uri::from("file:///file0.rs");
3680 assert!(
3681 cache.diagnostics(&server_oldest).is_none(),
3682 "the pre-existing server's oldest entry, now far over its shrunk share, must be evicted"
3683 );
3684 }
3685
3686 #[test]
3692 fn test_fair_share_applies_by_default_without_explicit_route_count() {
3693 let mut cache = NotificationCache::new();
3694 let noisy = ServerId::from_static("noisy");
3695 let quiet = ServerId::from_static("quiet");
3696
3697 let quiet_uri: Uri = Uri::from("file:///quiet/only_file.rs");
3698 cache.store_diagnostics(&quiet, &quiet_uri, Some(1), vec![]);
3699
3700 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 50 {
3702 let uri: Uri = Uri::from(format!("file:///noisy/file{i}.rs"));
3703 cache.store_diagnostics(&noisy, &uri, Some(1), vec![]);
3704 }
3705
3706 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3707 assert!(
3708 cache.diagnostics(&quiet_uri).is_some(),
3709 "quiet server's only entry must survive even without ever calling \
3710 set_diagnostics_route_count"
3711 );
3712 let noisy_first: Uri = Uri::from("file:///noisy/file0.rs");
3713 assert!(
3714 cache.diagnostics(&noisy_first).is_none(),
3715 "the noisy server, now auto-derived as one of two servers sharing the budget, \
3716 must still lose its own oldest entries once over its fair share"
3717 );
3718 }
3719
3720 #[test]
3726 fn test_single_server_gets_full_budget_without_explicit_route_count() {
3727 let mut cache = NotificationCache::new();
3728
3729 for i in 0..MAX_DIAGNOSTIC_ENTRIES {
3730 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3731 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3732 }
3733
3734 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3735 }
3736
3737 #[test]
3743 fn test_empty_diagnostics_entries_evicted_before_non_empty_ones() {
3744 let mut cache = NotificationCache::new();
3745 let server = test_server();
3746
3747 let important: Uri = Uri::from("file:///important.rs");
3748 cache.store_diagnostics(
3749 &server,
3750 &important,
3751 Some(1),
3752 vec![minimal_diagnostic("real error".to_string())],
3753 );
3754
3755 for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
3759 let uri: Uri = Uri::from(format!("file:///clean{i}.rs"));
3760 cache.store_diagnostics(&server, &uri, Some(1), vec![]);
3761 }
3762 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3763
3764 let overflow: Uri = Uri::from("file:///overflow.rs");
3768 cache.store_diagnostics(&server, &overflow, Some(1), vec![]);
3769
3770 assert!(
3771 cache.diagnostics(&important).is_some(),
3772 "a non-empty entry must survive eviction over empty entries, even though it is older"
3773 );
3774 let oldest_clean: Uri = Uri::from("file:///clean0.rs");
3775 assert!(
3776 cache.diagnostics(&oldest_clean).is_none(),
3777 "the oldest empty entry must be evicted instead of the older non-empty one"
3778 );
3779 assert!(cache.diagnostics(&overflow).is_some());
3780 }
3781
3782 #[test]
3790 fn test_empty_diagnostics_entry_is_still_tracked_until_evicted() {
3791 let mut cache = NotificationCache::new();
3792 let uri: Uri = Uri::from("file:///clean.rs");
3793
3794 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3795
3796 let stored = cache.diagnostics(&uri);
3797 assert!(
3798 stored.is_some(),
3799 "an empty-diagnostics entry must still be tracked"
3800 );
3801 assert_eq!(stored.unwrap().diagnostics.len(), 0);
3802 }
3803
3804 #[test]
3809 fn test_over_share_servers_empty_entry_evicted_before_a_different_servers_real_diagnostic() {
3810 let mut cache = NotificationCache::new();
3811 cache.set_diagnostics_route_count(3); let a = ServerId::from_static("a"); let b = ServerId::from_static("b"); let c = ServerId::from_static("c"); for i in 0..500 {
3818 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
3819 cache.store_diagnostics(
3820 &a,
3821 &uri,
3822 Some(1),
3823 vec![minimal_diagnostic(format!("error {i}"))],
3824 );
3825 }
3826 for i in 0..400 {
3827 let uri: Uri = Uri::from(format!("file:///b/file{i}.rs"));
3828 cache.store_diagnostics(&b, &uri, Some(1), vec![]);
3829 }
3830 for i in 0..100 {
3831 let uri: Uri = Uri::from(format!("file:///c/file{i}.rs"));
3832 cache.store_diagnostics(&c, &uri, Some(1), vec![]);
3833 }
3834 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3835
3836 let overflow: Uri = Uri::from("file:///a/overflow.rs");
3841 cache.store_diagnostics(
3842 &a,
3843 &overflow,
3844 Some(1),
3845 vec![minimal_diagnostic("overflow error".to_string())],
3846 );
3847
3848 for i in 0..500 {
3849 let uri: Uri = Uri::from(format!("file:///a/file{i}.rs"));
3850 assert!(
3851 cache.diagnostics(&uri).is_some(),
3852 "server a's real diagnostics must all survive; b has an empty entry to lose \
3853 instead"
3854 );
3855 }
3856 let b_oldest: Uri = Uri::from("file:///b/file0.rs");
3857 assert!(
3858 cache.diagnostics(&b_oldest).is_none(),
3859 "b's oldest empty entry must be evicted instead of a's real diagnostics"
3860 );
3861 assert!(cache.diagnostics(&overflow).is_some());
3862 }
3863
3864 #[test]
3869 fn test_dirty_then_clean_then_dirty_again_updates_emptiness_tracking() {
3870 let mut cache = NotificationCache::new();
3871 let server = test_server();
3872 let uri: Uri = Uri::from("file:///flapping.rs");
3873
3874 cache.store_diagnostics(
3875 &server,
3876 &uri,
3877 Some(1),
3878 vec![minimal_diagnostic("first error".to_string())],
3879 );
3880 cache.store_diagnostics(&server, &uri, Some(2), vec![]); cache.store_diagnostics(
3882 &server,
3883 &uri,
3884 Some(3),
3885 vec![minimal_diagnostic("second error".to_string())],
3886 ); for i in 0..MAX_DIAGNOSTIC_ENTRIES - 1 {
3892 let other: Uri = Uri::from(format!("file:///clean{i}.rs"));
3893 cache.store_diagnostics(&server, &other, Some(1), vec![]);
3894 }
3895 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3896
3897 let overflow: Uri = Uri::from("file:///overflow.rs");
3898 cache.store_diagnostics(&server, &overflow, Some(1), vec![]);
3899
3900 let stored = cache.diagnostics(&uri);
3901 assert!(
3902 stored.is_some_and(|info| info.diagnostics.len() == 1),
3903 "the re-dirtied entry must survive and keep its real diagnostic, not be mistaken \
3904 for an empty entry"
3905 );
3906 }
3907
3908 #[test]
3911 fn test_set_diagnostics_route_count_zero_clamps_to_one() {
3912 let mut cache = NotificationCache::new();
3913 cache.set_diagnostics_route_count(0);
3914
3915 for i in 0..MAX_DIAGNOSTIC_ENTRIES + 5 {
3916 let uri: Uri = Uri::from(format!("file:///file{i}.rs"));
3917 cache.store_diagnostics(&test_server(), &uri, Some(1), vec![]);
3918 }
3919
3920 assert_eq!(cache.diagnostics_count(), MAX_DIAGNOSTIC_ENTRIES);
3921 }
3922
3923 #[test]
3926 fn test_indexing_state_defaults_unknown() {
3927 let cache = NotificationCache::new();
3928 assert_eq!(cache.indexing_state(&test_server()), IndexingState::Unknown);
3929 }
3930
3931 #[test]
3932 fn test_observe_indexing_signal_delegates_to_tracker() {
3933 let mut cache = NotificationCache::new();
3934 let server = test_server();
3935 cache.observe_indexing_signal(
3936 &server,
3937 "experimental/serverStatus",
3938 Some(&serde_json::json!({"quiescent": false})),
3939 );
3940 assert_eq!(cache.indexing_state(&server), IndexingState::Loading);
3941 }
3942
3943 #[test]
3944 fn test_reset_indexing_state_delegates_to_tracker() {
3945 let mut cache = NotificationCache::new();
3946 let server = test_server();
3947 cache.observe_indexing_signal(
3948 &server,
3949 "experimental/serverStatus",
3950 Some(&serde_json::json!({"quiescent": false})),
3951 );
3952 cache.reset_indexing_state(&server, IndexingReset::Forget);
3953 assert_eq!(cache.indexing_state(&server), IndexingState::Unknown);
3954 }
3955
3956 fn file_uri(path: &str) -> Uri {
3957 Uri::from(format!("file:///ws/{path}"))
3958 }
3959
3960 fn published(source: &str) -> PublishedDiagnosticsUri {
3961 PublishedDiagnosticsUri::for_test(file_uri(source), file_uri("main.rs"))
3962 }
3963
3964 fn diagnostic_at(line: u32, message: &str) -> LspDiagnostic {
3965 let mut diagnostic = minimal_diagnostic(message.to_owned());
3966 diagnostic.range.start.line = line;
3967 diagnostic.range.end.line = line;
3968 diagnostic
3969 }
3970
3971 fn merged(cache: &NotificationCache) -> Option<DiagnosticInfo> {
3972 cache.diagnostic_sources(&file_uri("main.rs")).merge()
3973 }
3974
3975 fn messages(info: &DiagnosticInfo) -> Vec<String> {
3976 info.diagnostics
3977 .iter()
3978 .map(|d| message_as_str(&d.message).to_owned())
3979 .collect()
3980 }
3981
3982 #[test]
3985 fn test_published_alias_errors_survive_empty_canonical_publish() {
3986 let mut cache = NotificationCache::new();
3987 let server = test_server();
3988 let errors = vec![diagnostic_at(1, "e1"), diagnostic_at(2, "e2")];
3989
3990 cache.store_published_diagnostics(&server, &published("alias.rs"), None, errors);
3991 cache.store_published_diagnostics(&server, &published("main.rs"), Some(3), vec![]);
3992 assert_eq!(messages(&merged(&cache).unwrap()), ["e1", "e2"]);
3993
3994 cache.store_published_diagnostics(&server, &published("main.rs"), Some(4), vec![]);
3995 assert_eq!(messages(&merged(&cache).unwrap()), ["e1", "e2"]);
3996
3997 cache.store_published_diagnostics(&server, &published("alias.rs"), None, vec![]);
3998 assert!(merged(&cache).unwrap().diagnostics.is_empty());
3999 }
4000
4001 #[test]
4002 fn test_published_empty_publish_clears_only_its_own_source() {
4003 let mut cache = NotificationCache::new();
4004 let server = test_server();
4005 cache.store_published_diagnostics(
4006 &server,
4007 &published("a1.rs"),
4008 None,
4009 vec![diagnostic_at(1, "e1")],
4010 );
4011 cache.store_published_diagnostics(
4012 &server,
4013 &published("a2.rs"),
4014 None,
4015 vec![diagnostic_at(2, "e2")],
4016 );
4017 assert_eq!(messages(&merged(&cache).unwrap()), ["e1", "e2"]);
4018
4019 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![]);
4020
4021 assert_eq!(messages(&merged(&cache).unwrap()), ["e2"]);
4022 }
4023
4024 #[test]
4026 fn test_merge_removes_duplicates_sharing_a_start_with_another_diagnostic() {
4027 let mut cache = NotificationCache::new();
4028 let server = test_server();
4029 let x = diagnostic_at(1, "x");
4030 let y = diagnostic_at(1, "y");
4031 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![x.clone(), y]);
4032 cache.store_published_diagnostics(&server, &published("a2.rs"), None, vec![x]);
4033
4034 assert_eq!(messages(&merged(&cache).unwrap()), ["x", "y"]);
4035 }
4036
4037 #[test]
4038 fn test_merge_orders_by_range_start() {
4039 let mut cache = NotificationCache::new();
4040 let server = test_server();
4041 cache.store_published_diagnostics(
4042 &server,
4043 &published("a1.rs"),
4044 None,
4045 vec![diagnostic_at(5, "late")],
4046 );
4047 cache.store_published_diagnostics(
4048 &server,
4049 &published("a2.rs"),
4050 None,
4051 vec![diagnostic_at(1, "early")],
4052 );
4053
4054 assert_eq!(messages(&merged(&cache).unwrap()), ["early", "late"]);
4055 }
4056
4057 #[test]
4058 fn test_merged_version_is_the_canonical_sources() {
4059 let mut cache = NotificationCache::new();
4060 let server = test_server();
4061 cache.store_published_diagnostics(
4062 &server,
4063 &published("main.rs"),
4064 Some(7),
4065 vec![diagnostic_at(1, "a")],
4066 );
4067 cache.store_published_diagnostics(
4068 &server,
4069 &published("alias.rs"),
4070 None,
4071 vec![diagnostic_at(2, "b")],
4072 );
4073 assert_eq!(
4074 merged(&cache).unwrap().version.map(DocumentVersion::get),
4075 Some(7)
4076 );
4077
4078 cache.store_published_diagnostics(
4079 &server,
4080 &published("main.rs"),
4081 Some(8),
4082 vec![diagnostic_at(1, "a")],
4083 );
4084 assert_eq!(
4085 merged(&cache).unwrap().version.map(DocumentVersion::get),
4086 Some(8)
4087 );
4088 }
4089
4090 #[test]
4091 fn test_merge_is_bounded_by_the_entry_size_cap() {
4092 let mut cache = NotificationCache::new();
4093 let server = test_server();
4094 let big = "x".repeat(200 * 1024);
4095 for source in [
4096 "a1.rs", "a2.rs", "a3.rs", "a4.rs", "a5.rs", "a6.rs", "a7.rs", "a8.rs",
4097 ] {
4098 let diagnostics = (0..4)
4099 .map(|n| diagnostic_at(n, &format!("{source}-{n}-{big}")))
4100 .collect();
4101 cache.store_published_diagnostics(&server, &published(source), None, diagnostics);
4102 }
4103
4104 let info = merged(&cache).unwrap();
4105
4106 let bytes = serde_json::to_vec(&info.diagnostics).unwrap().len();
4107 assert!(bytes <= MAX_DIAGNOSTICS_ENTRY_BYTES, "{bytes} bytes");
4108 }
4109
4110 #[test]
4111 fn test_single_source_is_returned_unchanged() {
4112 let mut cache = NotificationCache::new();
4113 cache.store_published_diagnostics(
4114 &test_server(),
4115 &published("alias.rs"),
4116 Some(2),
4117 vec![diagnostic_at(1, "e")],
4118 );
4119
4120 let info = merged(&cache).unwrap();
4121
4122 assert_eq!(info.uri, file_uri("alias.rs"));
4123 assert_eq!(info.version.map(DocumentVersion::get), Some(2));
4124 }
4125
4126 #[test]
4127 fn test_ninth_source_is_dropped_without_an_orphan_entry() {
4128 let mut cache = NotificationCache::new();
4129 let server = test_server();
4130 for n in 0..MAX_SOURCES_PER_FILE {
4131 cache.store_published_diagnostics(
4132 &server,
4133 &published(&format!("a{n}.rs")),
4134 None,
4135 vec![diagnostic_at(1, "e")],
4136 );
4137 }
4138
4139 cache.store_published_diagnostics(
4140 &server,
4141 &published("extra.rs"),
4142 None,
4143 vec![diagnostic_at(9, "extra")],
4144 );
4145
4146 assert_eq!(cache.diagnostics_count(), MAX_SOURCES_PER_FILE);
4147 assert!(cache.diagnostics(&file_uri("extra.rs")).is_none());
4148 assert!(!messages(&merged(&cache).unwrap()).contains(&"extra".to_owned()));
4149 }
4150
4151 #[test]
4154 fn test_lost_publish_replaces_older_entry_with_a_lost_mark() {
4155 let mut cache = NotificationCache::new();
4156 let server = test_server();
4157 let uri = file_uri("a.rs");
4158 cache.store_diagnostics(&server, &uri, None, vec![diagnostic_at(1, "old")]);
4159
4160 let changed = cache.record_lost_files(&server, &[DiagnosticsKey::of(&uri)]);
4161
4162 assert_eq!(changed, [DiagnosticsKey::of(&uri)]);
4163 assert!(!cache.has_diagnostics(&uri));
4164 assert_eq!(
4165 cache.availability(&uri, Some(&server)),
4166 DiagnosticsAvailability::Evicted
4167 );
4168 cache.store_diagnostics(&server, &uri, None, vec![diagnostic_at(2, "new")]);
4169 assert_eq!(
4170 cache.availability(&uri, Some(&server)),
4171 DiagnosticsAvailability::Published
4172 );
4173 cache.assert_consistent();
4174 }
4175
4176 #[test]
4178 fn test_unnamed_lost_publishes_make_unknown_files_read_evicted() {
4179 let mut cache = NotificationCache::new();
4180 let server = test_server();
4181
4182 cache.record_unnamed_loss(&server);
4183
4184 assert_eq!(
4185 cache.availability(&file_uri("unknown.rs"), Some(&server)),
4186 DiagnosticsAvailability::Evicted
4187 );
4188 }
4189
4190 #[test]
4192 fn test_a_publish_over_the_source_cap_is_reported_as_dropped() {
4193 let mut cache = NotificationCache::new();
4194 let server = test_server();
4195 for n in 0..MAX_SOURCES_PER_FILE {
4196 let write = cache.write_published_diagnostics(
4197 &server,
4198 &published(&format!("a{n}.rs")),
4199 None,
4200 BoundedDiagnostics::new(&file_uri("a.rs"), vec![diagnostic_at(1, "e")]),
4201 );
4202 std::assert_matches!(write, PushWrite::Stored { .. });
4203 }
4204
4205 let write = cache.write_published_diagnostics(
4206 &server,
4207 &published("extra.rs"),
4208 None,
4209 BoundedDiagnostics::new(&file_uri("a.rs"), vec![diagnostic_at(9, "extra")]),
4210 );
4211
4212 std::assert_matches!(write, PushWrite::Dropped { .. });
4213 }
4214
4215 #[test]
4216 fn test_republishing_an_indexed_source_at_the_cap_is_accepted() {
4217 let mut cache = NotificationCache::new();
4218 let server = test_server();
4219 for n in 0..MAX_SOURCES_PER_FILE {
4220 cache.store_published_diagnostics(
4221 &server,
4222 &published(&format!("a{n}.rs")),
4223 None,
4224 vec![],
4225 );
4226 }
4227
4228 cache.store_published_diagnostics(
4229 &server,
4230 &published("a0.rs"),
4231 None,
4232 vec![diagnostic_at(1, "again")],
4233 );
4234
4235 assert_eq!(messages(&merged(&cache).unwrap()), ["again"]);
4236 }
4237
4238 #[test]
4239 fn test_clear_server_diagnostics_prunes_the_source_index() {
4240 let mut cache = NotificationCache::new();
4241 let server = test_server();
4242 cache.store_published_diagnostics(
4243 &server,
4244 &published("a1.rs"),
4245 None,
4246 vec![diagnostic_at(1, "e")],
4247 );
4248
4249 cache.clear_server_diagnostics(&server);
4250
4251 cache.assert_consistent();
4252 assert!(cache.files.is_empty());
4253 assert!(!cache.has_diagnostics(&file_uri("main.rs")));
4254 assert!(merged(&cache).is_none());
4255 }
4256
4257 #[test]
4258 fn test_eviction_prunes_the_source_index() {
4259 let mut cache = NotificationCache::new();
4260 let server = test_server();
4261 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![]);
4262 for n in 0..MAX_DIAGNOSTIC_ENTRIES {
4263 let uri = Uri::from(format!("file:///other/{n}.rs"));
4264 cache.store_diagnostics(&server, &uri, None, vec![]);
4265 }
4266
4267 assert!(cache.diagnostics(&file_uri("a1.rs")).is_none());
4268 cache.assert_consistent();
4269 assert!(
4270 !cache
4271 .files
4272 .contains_key(&DiagnosticsKey::of(&file_uri("main.rs")))
4273 );
4274 }
4275
4276 #[test]
4277 fn test_diagnostics_owner_resolves_through_the_source_index() {
4278 let mut cache = NotificationCache::new();
4279 let server = test_server();
4280 cache.store_published_diagnostics(&server, &published("alias.rs"), None, vec![]);
4281
4282 assert_eq!(cache.diagnostics_owner(&file_uri("main.rs")), Some(&server));
4283 }
4284
4285 #[test]
4288 fn test_canonical_source_is_admitted_when_aliases_fill_the_cap() {
4289 let mut cache = NotificationCache::new();
4290 let server = test_server();
4291 for n in 0..MAX_SOURCES_PER_FILE {
4292 cache.store_published_diagnostics(
4293 &server,
4294 &published(&format!("a{n}.rs")),
4295 None,
4296 vec![diagnostic_at(
4297 u32::try_from(n).unwrap(),
4298 &format!("alias{n}"),
4299 )],
4300 );
4301 }
4302
4303 cache.store_published_diagnostics(
4304 &server,
4305 &published("main.rs"),
4306 Some(5),
4307 vec![diagnostic_at(99, "canonical")],
4308 );
4309
4310 let info = merged(&cache).unwrap();
4311 let shown = messages(&info);
4312 assert!(shown.contains(&"canonical".to_owned()), "{shown:?}");
4313 assert!(
4314 !shown.contains(&"alias0".to_owned()),
4315 "oldest alias is evicted"
4316 );
4317 assert_eq!(shown.len(), MAX_SOURCES_PER_FILE);
4318 assert_eq!(info.version.map(DocumentVersion::get), Some(5));
4319 assert_eq!(cache.diagnostics_count(), MAX_SOURCES_PER_FILE);
4320 }
4321
4322 #[test]
4323 fn test_alias_beyond_the_cap_is_still_dropped_when_canonical_is_present() {
4324 let mut cache = NotificationCache::new();
4325 let server = test_server();
4326 cache.store_published_diagnostics(&server, &published("main.rs"), None, vec![]);
4327 for n in 0..MAX_SOURCES_PER_FILE - 1 {
4328 cache.store_published_diagnostics(
4329 &server,
4330 &published(&format!("a{n}.rs")),
4331 None,
4332 vec![],
4333 );
4334 }
4335
4336 cache.store_published_diagnostics(
4337 &server,
4338 &published("extra.rs"),
4339 None,
4340 vec![diagnostic_at(1, "x")],
4341 );
4342
4343 assert!(cache.diagnostics(&file_uri("extra.rs")).is_none());
4344 assert!(cache.diagnostics(&file_uri("main.rs")).is_some());
4345 }
4346
4347 #[test]
4350 fn test_merge_handles_many_same_start_diagnostics_across_sources() {
4351 const PER_SOURCE: u32 = 3000;
4352 let mut cache = NotificationCache::new();
4353 let server = test_server();
4354 for source in 0..MAX_SOURCES_PER_FILE {
4355 let diagnostics = (0..PER_SOURCE)
4356 .map(|n| diagnostic_at(0, &format!("diagnostic-{n}")))
4357 .collect();
4358 cache.store_published_diagnostics(
4359 &server,
4360 &published(&format!("a{source}.rs")),
4361 None,
4362 diagnostics,
4363 );
4364 }
4365
4366 let info = merged(&cache).unwrap();
4367
4368 assert_eq!(info.diagnostics.len(), PER_SOURCE as usize);
4369 }
4370
4371 #[test]
4374 fn test_merge_keeps_diagnostics_that_differ_outside_range_and_message() {
4375 let mut cache = NotificationCache::new();
4376 let server = test_server();
4377 let plain = diagnostic_at(1, "same");
4378 let mut coded = diagnostic_at(1, "same");
4379 coded.code = Some(lsp_types::Code::String("E1".to_owned()));
4380 cache.store_published_diagnostics(&server, &published("a1.rs"), None, vec![plain]);
4381 cache.store_published_diagnostics(&server, &published("a2.rs"), None, vec![coded]);
4382
4383 assert_eq!(merged(&cache).unwrap().diagnostics.len(), 2);
4384 }
4385
4386 #[test]
4387 fn test_clear_server_diagnostics_leaves_no_dangling_file_index() {
4388 let mut cache = NotificationCache::new();
4389 let (a, b) = (ServerId::from_static("a"), ServerId::from_static("b"));
4390 cache.store_published_diagnostics(&a, &published("a1.rs"), None, vec![]);
4391 cache.store_published_diagnostics(
4392 &b,
4393 &published("main.rs"),
4394 None,
4395 vec![diagnostic_at(1, "e")],
4396 );
4397
4398 cache.clear_server_diagnostics(&a);
4399
4400 cache.assert_consistent();
4401 assert_eq!(cache.files.len(), 1);
4402 assert!(cache.diagnostics(&file_uri("main.rs")).is_some());
4403 assert!(cache.diagnostics(&file_uri("a1.rs")).is_none());
4404 }
4405
4406 #[derive(Debug, Clone)]
4407 enum CacheOp {
4408 Publish {
4409 server: u8,
4410 file: u8,
4411 alias: Option<u8>,
4412 empty: bool,
4413 },
4414 Clear {
4415 server: u8,
4416 },
4417 }
4418
4419 fn cache_op() -> impl proptest::strategy::Strategy<Value = CacheOp> {
4420 use proptest::prelude::*;
4421
4422 prop_oneof![
4423 4 => (0..3u8, 0..3u8, proptest::option::of(0..3u8), any::<bool>()).prop_map(
4424 |(server, file, alias, empty)| CacheOp::Publish { server, file, alias, empty }
4425 ),
4426 1 => (0..3u8).prop_map(|server| CacheOp::Clear { server }),
4427 ]
4428 }
4429
4430 proptest::proptest! {
4431 #[test]
4432 fn cache_indices_stay_consistent_under_random_operations(
4433 ops in proptest::collection::vec(cache_op(), 0..60)
4434 ) {
4435 let mut cache = NotificationCache::new();
4436 for op in ops {
4437 match op {
4438 CacheOp::Publish { server, file, alias, empty } => {
4439 let canonical = file_uri(&format!("f{file}.rs"));
4440 let source = alias.map_or_else(
4441 || canonical.clone(),
4442 |n| file_uri(&format!("alias{n}.rs")),
4443 );
4444 let diagnostics = if empty { vec![] } else { vec![diagnostic_at(1, "e")] };
4445 cache.store_published_diagnostics(
4446 &ServerId::new(format!("s{server}")).unwrap(),
4447 &PublishedDiagnosticsUri::for_test(source, canonical),
4448 None,
4449 diagnostics,
4450 );
4451 }
4452 CacheOp::Clear { server } => {
4453 cache.clear_server_diagnostics(&ServerId::new(format!("s{server}")).unwrap());
4454 }
4455 }
4456 cache.assert_consistent();
4457 }
4458 }
4459 }
4460
4461 fn first_uri() -> Uri {
4462 Uri::from("file:///first.rs")
4463 }
4464
4465 fn error_diagnostic() -> LspDiagnostic {
4466 LspDiagnostic {
4467 severity: Some(lsp_types::DiagnosticSeverity::Error),
4468 message: "broken".to_owned().into(),
4469 ..LspDiagnostic::default()
4470 }
4471 }
4472
4473 fn full_cache(first_empty: bool) -> NotificationCache {
4476 let mut cache = NotificationCache::new();
4477 let first: Uri = Uri::from("file:///first.rs");
4478 let diagnostics = if first_empty {
4479 vec![]
4480 } else {
4481 vec![error_diagnostic()]
4482 };
4483 cache.store_diagnostics(&test_server(), &first, None, diagnostics);
4484 for i in 1..MAX_DIAGNOSTIC_ENTRIES {
4485 let uri: Uri = Uri::from(format!("file:///filler{i}.rs"));
4486 cache.store_diagnostics(&test_server(), &uri, None, vec![error_diagnostic()]);
4487 }
4488 cache
4489 }
4490
4491 fn evict_one(cache: &mut NotificationCache) {
4492 let overflow: Uri = Uri::from("file:///overflow.rs");
4493 cache.store_diagnostics(&test_server(), &overflow, None, vec![error_diagnostic()]);
4494 }
4495
4496 #[test]
4497 fn test_evicted_empty_entry_is_listen_replayable() {
4498 let mut cache = full_cache(true);
4499 assert!(cache.is_listen_replayable(&first_uri()));
4500 evict_one(&mut cache);
4501 assert!(!cache.has_diagnostics(&first_uri()));
4502 assert!(cache.is_listen_replayable(&first_uri()));
4503 assert!(!cache.is_listen_replayable(&Uri::from("file:///unrelated.rs")));
4504 }
4505
4506 fn write_error(cache: &mut NotificationCache, file: &str) -> PushWrite {
4507 let uri = Uri::from(file);
4508 cache.write_published_diagnostics(
4509 &test_server(),
4510 &PublishedDiagnosticsUri::for_test(uri.clone(), uri),
4511 None,
4512 BoundedDiagnostics::new(&Uri::from(file), vec![error_diagnostic()]),
4513 )
4514 }
4515
4516 #[test]
4518 fn test_a_push_reports_the_file_it_evicts() {
4519 let mut cache = full_cache(false);
4520 let write = write_error(&mut cache, "file:///overflow.rs");
4521 assert_eq!(write.evicted(), [DiagnosticsKey::of(&first_uri())]);
4522 assert!(!cache.has_diagnostics(&first_uri()));
4523 }
4524
4525 #[test]
4527 fn test_a_push_that_needs_no_room_reports_no_eviction() {
4528 let mut cache = full_cache(false);
4529 assert!(
4530 write_error(&mut cache, "file:///first.rs")
4531 .evicted()
4532 .is_empty()
4533 );
4534 let mut roomy = NotificationCache::new();
4535 assert!(write_error(&mut roomy, "file:///a.rs").evicted().is_empty());
4536 }
4537
4538 fn route() -> ServerId {
4539 test_server()
4540 }
4541
4542 #[test]
4545 fn test_evicted_clean_entry_reads_as_published() {
4546 let mut cache = full_cache(true);
4547 assert!(cache.has_diagnostics(&first_uri()));
4548 evict_one(&mut cache);
4549 assert!(!cache.has_diagnostics(&first_uri()));
4550 assert_eq!(
4551 cache.availability(&first_uri(), Some(&route())),
4552 DiagnosticsAvailability::Published
4553 );
4554 }
4555
4556 #[test]
4559 fn test_evicted_non_empty_entry_reads_as_evicted() {
4560 let mut cache = full_cache(false);
4561 evict_one(&mut cache);
4562 assert_eq!(
4563 cache.availability(&first_uri(), Some(&route())),
4564 DiagnosticsAvailability::Evicted
4565 );
4566 }
4567
4568 #[test]
4569 fn test_unpublished_file_reads_as_pending_even_next_to_evictions() {
4570 let mut cache = full_cache(false);
4571 evict_one(&mut cache);
4572 let unseen = Uri::from("file:///never-published.rs");
4573 assert_eq!(
4574 cache.availability(&unseen, Some(&route())),
4575 DiagnosticsAvailability::Pending
4576 );
4577 assert_eq!(
4578 cache.availability(&unseen, None),
4579 DiagnosticsAvailability::Pending
4580 );
4581 }
4582
4583 #[test]
4586 fn test_clearing_a_server_forgets_its_eviction_marks() {
4587 let mut cache = full_cache(false);
4588 evict_one(&mut cache);
4589 drop(cache.clear_server_diagnostics(&route()));
4590 assert_eq!(
4591 cache.availability(&first_uri(), Some(&route())),
4592 DiagnosticsAvailability::Pending
4593 );
4594 }
4595
4596 #[test]
4597 fn test_a_new_publish_replaces_the_mark_of_its_file() {
4598 let mut cache = full_cache(true);
4599 evict_one(&mut cache);
4600 let first: Uri = Uri::from("file:///first.rs");
4601 cache.store_diagnostics(&test_server(), &first, None, vec![error_diagnostic()]);
4602 assert_eq!(
4603 cache.availability(&first_uri(), None),
4604 DiagnosticsAvailability::Published
4605 );
4606 assert!(
4607 cache
4608 .eviction_marks
4609 .content(&DiagnosticsKey::of(&first))
4610 .is_none()
4611 );
4612 }
4613
4614 #[test]
4617 fn test_lost_content_survives_a_later_clean_eviction_of_the_file() {
4618 let mut marks = EvictionMarks::default();
4619 let file = DiagnosticsKey::of(&first_uri());
4620 marks.record(file.clone(), route(), EvictedContent::Lost);
4621 marks.record(file.clone(), route(), EvictedContent::Clean);
4622 assert_eq!(marks.content(&file), Some(EvictedContent::Lost));
4623 }
4624
4625 fn overflowed_after_churn() -> NotificationCache {
4626 let mut cache = full_cache(false);
4627 for i in 0..=MAX_EVICTION_MARKS {
4628 drop(write_error(&mut cache, &format!("file:///churn{i}.rs")));
4629 }
4630 assert!(cache.eviction_marks.has_overflowed(&route()));
4631 cache
4632 }
4633
4634 #[test]
4637 fn test_overflow_ends_with_the_owners_next_accepted_push() {
4638 let mut cache = NotificationCache::new();
4639 cache.eviction_marks.overflowed.insert(route());
4640 let unseen = Uri::from("file:///never-published.rs");
4641 assert_eq!(
4642 cache.availability(&unseen, Some(&route())),
4643 DiagnosticsAvailability::Evicted
4644 );
4645
4646 drop(write_error(&mut cache, "file:///fresh.rs"));
4647
4648 assert!(!cache.eviction_marks.has_overflowed(&route()));
4649 assert_eq!(
4650 cache.availability(&unseen, Some(&route())),
4651 DiagnosticsAvailability::Pending
4652 );
4653 }
4654
4655 #[test]
4658 fn test_overflow_ends_with_the_owners_next_accepted_pull_only() {
4659 let mut cache = NotificationCache::new();
4660 cache.eviction_marks.overflowed.insert(route());
4661 let other = ServerId::from_static("other-server");
4662 cache.store_pulled_for_test(&other, &Uri::from("file:///other.rs"), Vec::new());
4663 assert!(cache.eviction_marks.has_overflowed(&route()));
4664
4665 cache.store_pulled_for_test(&route(), &Uri::from("file:///pulled.rs"), Vec::new());
4666 assert!(!cache.eviction_marks.has_overflowed(&route()));
4667 }
4668
4669 #[test]
4673 fn test_overflow_clear_precedes_the_evictions_of_the_same_write() {
4674 let mut cache = overflowed_after_churn();
4675 drop(write_error(&mut cache, "file:///another-new.rs"));
4676 assert!(cache.eviction_marks.has_overflowed(&route()));
4677
4678 drop(write_error(&mut cache, "file:///another-new.rs"));
4679 assert!(!cache.eviction_marks.has_overflowed(&route()));
4680 cache.assert_consistent();
4681 }
4682
4683 #[test]
4687 fn test_overflowing_the_marks_makes_unmarked_files_read_as_evicted_for_that_server() {
4688 let mut cache = full_cache(false);
4689 for i in 0..=MAX_EVICTION_MARKS {
4690 drop(write_error(&mut cache, &format!("file:///churn{i}.rs")));
4691 }
4692 assert!(cache.eviction_marks.has_overflowed(&route()));
4693 assert!(cache.eviction_marks.by_file.len() <= MAX_EVICTION_MARKS);
4694 assert!(cache.eviction_marks.order.len() <= MAX_EVICTION_MARKS);
4695 cache.assert_consistent();
4696
4697 assert_eq!(
4698 cache.availability(&first_uri(), Some(&route())),
4699 DiagnosticsAvailability::Evicted,
4700 "its mark was the oldest, forgotten"
4701 );
4702 let unseen = Uri::from("file:///never-published.rs");
4703 assert_eq!(
4704 cache.availability(&unseen, Some(&route())),
4705 DiagnosticsAvailability::Evicted
4706 );
4707 assert_eq!(
4708 cache.availability(&unseen, Some(&ServerId::from_static("other"))),
4709 DiagnosticsAvailability::Pending
4710 );
4711
4712 drop(cache.clear_server_diagnostics(&route()));
4713 assert_eq!(
4714 cache.availability(&unseen, Some(&route())),
4715 DiagnosticsAvailability::Pending
4716 );
4717 }
4718
4719 #[test]
4720 fn test_evicted_non_empty_entry_is_not_replayable() {
4721 let mut cache = full_cache(false);
4722 evict_one(&mut cache);
4723 assert!(!cache.has_diagnostics(&first_uri()));
4724 assert!(!cache.is_listen_replayable(&first_uri()));
4725 }
4726
4727 #[test]
4728 fn test_recached_entry_leaves_the_replay_ring_view() {
4729 let mut cache = full_cache(true);
4730 evict_one(&mut cache);
4731 let first: Uri = Uri::from("file:///first.rs");
4732 cache.store_diagnostics(&test_server(), &first, None, vec![error_diagnostic()]);
4733 assert!(cache.has_diagnostics(&first_uri()));
4734 assert!(!cache.was_recently_evicted(&first_uri(), std::time::Instant::now()));
4735 }
4736
4737 #[test]
4738 fn test_eviction_replay_expires_after_the_window() {
4739 let mut cache = NotificationCache::new();
4740 let now = std::time::Instant::now();
4741 let gone = Uri::from("file:///gone.rs");
4742 cache.record_empty_eviction(DiagnosticsKey::of(&gone), now);
4743 assert!(cache.was_recently_evicted(&gone, now + EVICTION_REPLAY_WINDOW));
4744 assert!(!cache.was_recently_evicted(
4745 &gone,
4746 now + EVICTION_REPLAY_WINDOW + std::time::Duration::from_secs(1)
4747 ));
4748 }
4749
4750 #[test]
4751 fn test_eviction_record_is_bounded_and_keeps_the_newest() {
4752 let mut cache = NotificationCache::new();
4753 let now = std::time::Instant::now();
4754 for i in 0..MAX_RECENT_EVICTIONS + 44 {
4755 let gone = Uri::from(format!("file:///gone{i}.rs"));
4756 cache.record_empty_eviction(
4757 DiagnosticsKey::of(&gone),
4758 now + std::time::Duration::from_millis(u64::try_from(i).unwrap()),
4759 );
4760 }
4761 assert_eq!(cache.recent_evictions.len(), MAX_RECENT_EVICTIONS);
4762 let at = now + std::time::Duration::from_secs(1);
4763 assert!(!cache.was_recently_evicted(&Uri::from("file:///gone0.rs"), at));
4764 assert!(!cache.was_recently_evicted(&Uri::from("file:///gone43.rs"), at));
4765 assert!(cache.was_recently_evicted(&Uri::from("file:///gone44.rs"), at));
4766 let newest = MAX_RECENT_EVICTIONS + 43;
4767 assert!(cache.was_recently_evicted(&Uri::from(format!("file:///gone{newest}.rs")), at));
4768 }
4769
4770 #[test]
4771 fn test_clearing_a_full_cache_keeps_every_empty_clear_replayable() {
4772 let mut cache = NotificationCache::new();
4773 let server = test_server();
4774 let uris: Vec<Uri> = (0..MAX_DIAGNOSTIC_ENTRIES)
4775 .map(|i| Uri::from(format!("file:///clean{i}.rs")))
4776 .collect();
4777 for uri in &uris {
4778 cache.store_diagnostics(&server, uri, None, vec![]);
4779 }
4780
4781 cache.clear_server_diagnostics(&server);
4782
4783 assert!(uris.iter().all(|uri| cache.is_listen_replayable(uri)));
4784 }
4785
4786 #[test]
4787 fn test_eviction_order_stays_within_twice_the_record_under_churn() {
4788 let mut cache = NotificationCache::new();
4789 let now = std::time::Instant::now();
4790 let keys: Vec<DiagnosticsKey> = (0..3)
4791 .map(|i| DiagnosticsKey::of(&Uri::from(format!("file:///hot{i}.rs"))))
4792 .collect();
4793 for tick in 0..10_000u64 {
4794 let key = keys[usize::try_from(tick % 3).unwrap()].clone();
4795 cache.record_empty_eviction(key, now + std::time::Duration::from_micros(tick));
4796 let record = &cache.recent_evictions;
4797 assert!(record.at.len() <= MAX_RECENT_EVICTIONS);
4798 assert!(record.order.len() <= 2 * record.at.len());
4799 }
4800 assert_eq!(cache.recent_evictions.len(), 3);
4801 }
4802
4803 #[test]
4804 fn test_eviction_record_stays_bounded_under_churn_past_the_cap() {
4805 const KEYS: usize = MAX_RECENT_EVICTIONS + 300;
4806 let mut cache = NotificationCache::new();
4807 let now = std::time::Instant::now();
4808 let keys: Vec<DiagnosticsKey> = (0..KEYS)
4809 .map(|i| DiagnosticsKey::of(&Uri::from(format!("file:///churn{i}.rs"))))
4810 .collect();
4811 for tick in 0..KEYS * 8 {
4812 let key = keys[tick % KEYS].clone();
4813 let at = now + std::time::Duration::from_micros(u64::try_from(tick).unwrap());
4814 cache.record_empty_eviction(key, at);
4815 let record = &cache.recent_evictions;
4816 assert!(record.at.len() <= MAX_RECENT_EVICTIONS);
4817 assert!(record.order.len() <= 2 * record.at.len() + 1);
4818 }
4819 assert_eq!(cache.recent_evictions.len(), MAX_RECENT_EVICTIONS);
4820 }
4821
4822 #[test]
4823 fn test_compaction_keeps_the_oldest_first_expiry_order() {
4824 let mut record = EvictionRecord::default();
4825 let now = std::time::Instant::now();
4826 let a = DiagnosticsKey::of(&Uri::from("file:///a.rs"));
4827 let b = DiagnosticsKey::of(&Uri::from("file:///b.rs"));
4828 record.record(a.clone(), now);
4829 record.record(b.clone(), now + std::time::Duration::from_secs(1));
4830 record.record(a.clone(), now + std::time::Duration::from_secs(2));
4831 record.record(b.clone(), now + std::time::Duration::from_secs(3));
4832 record.compact();
4833 let order: Vec<_> = record.order.iter().map(|(key, _)| key.clone()).collect();
4834 assert_eq!(order, vec![a, b]);
4835 }
4836
4837 #[test]
4838 fn test_eviction_record_overflow_warns_once_per_window() {
4839 use tracing_subscriber::layer::SubscriberExt as _;
4840
4841 let mut record = EvictionRecord::default();
4842 let now = std::time::Instant::now();
4843 let captured = CapturedLogs::default();
4844 let subscriber = tracing_subscriber::registry().with(captured.clone());
4845 let guard = tracing::subscriber::set_default(subscriber);
4846 for i in 0..MAX_RECENT_EVICTIONS + 10 {
4847 let key = DiagnosticsKey::of(&Uri::from(format!("file:///gone{i}.rs")));
4848 record.record(
4849 key,
4850 now + std::time::Duration::from_millis(u64::try_from(i).unwrap()),
4851 );
4852 }
4853 drop(guard);
4854
4855 let warnings = captured
4856 .messages()
4857 .iter()
4858 .filter(|m| m.contains("diagnostics clears evicted"))
4859 .count();
4860 assert_eq!(warnings, 1);
4861 }
4862
4863 #[test]
4864 fn test_eviction_record_deduplicates_and_prunes_expired_entries() {
4865 let mut cache = NotificationCache::new();
4866 let now = std::time::Instant::now();
4867 let a = DiagnosticsKey::of(&Uri::from("file:///a.rs"));
4868 cache.record_empty_eviction(a.clone(), now);
4869 cache.record_empty_eviction(a, now);
4870 assert_eq!(cache.recent_evictions.len(), 1);
4871 let later = now + EVICTION_REPLAY_WINDOW + std::time::Duration::from_secs(1);
4872 cache.record_empty_eviction(DiagnosticsKey::of(&Uri::from("file:///b.rs")), later);
4873 assert_eq!(cache.recent_evictions.len(), 1);
4874 }
4875
4876 #[test]
4877 fn test_alias_eviction_of_an_empty_entry_is_recorded() {
4878 let mut cache = NotificationCache::new();
4879 let server = test_server();
4880 for n in 0..MAX_SOURCES_PER_FILE {
4881 cache.store_published_diagnostics(
4882 &server,
4883 &published(&format!("a{n}.rs")),
4884 None,
4885 vec![],
4886 );
4887 }
4888 assert!(cache.recent_evictions.is_empty());
4889
4890 cache.store_published_diagnostics(
4891 &server,
4892 &PublishedDiagnosticsUri::for_test(file_uri("main.rs"), file_uri("main.rs")),
4893 None,
4894 vec![diagnostic_at(1, "e")],
4895 );
4896
4897 assert!(cache.diagnostics(&file_uri("a0.rs")).is_none());
4898 assert_eq!(cache.recent_evictions.len(), 1);
4899 assert!(cache.is_listen_replayable(&file_uri("main.rs")));
4900 }
4901
4902 #[test]
4903 fn test_clearing_a_server_records_its_empty_entries() {
4904 let mut cache = NotificationCache::new();
4905 let empty = file_uri("empty.rs");
4906 let broken = file_uri("broken.rs");
4907 cache.store_diagnostics(&test_server(), &empty, None, vec![]);
4908 cache.store_diagnostics(&test_server(), &broken, None, vec![error_diagnostic()]);
4909
4910 let cleared = cache.clear_server_diagnostics(&test_server());
4911
4912 assert_eq!(cleared.len(), 2);
4913 assert!(cache.is_listen_replayable(&empty));
4914 assert!(!cache.is_listen_replayable(&broken));
4915 }
4916}