use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use lsp_types::Location;
pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
pub const DEFAULT_CAPACITY: usize = 1_000;
pub trait Clock: Send + Sync {
fn now(&self) -> Instant;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> Instant {
Instant::now()
}
}
#[derive(Debug)]
pub struct MockClock {
inner: Mutex<Instant>,
}
impl MockClock {
#[must_use]
pub fn new() -> Self {
Self {
inner: Mutex::new(Instant::now()),
}
}
#[must_use]
pub fn with_start(start: Instant) -> Self {
Self {
inner: Mutex::new(start),
}
}
pub fn advance(&self, dur: Duration) {
let mut guard = self.inner.lock().expect("MockClock mutex poisoned");
*guard = guard
.checked_add(dur)
.expect("MockClock::advance overflowed Instant");
}
}
impl Default for MockClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for MockClock {
fn now(&self) -> Instant {
*self.inner.lock().expect("MockClock mutex poisoned")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub uri: String,
pub line: u32,
pub column: u32,
}
impl CacheKey {
#[must_use]
pub fn new(uri: String, line: u32, column: u32) -> Self {
Self { uri, line, column }
}
}
pub struct ReferencesCache {
inner: Mutex<CacheInner>,
clock: Arc<dyn Clock>,
ttl: Duration,
capacity: usize,
}
struct CacheInner {
entries: HashMap<CacheKey, (Instant, Vec<Location>)>,
lru: VecDeque<CacheKey>,
by_uri: HashMap<String, HashSet<CacheKey>>,
}
impl ReferencesCache {
pub const DEFAULT_CAPACITY: usize = DEFAULT_CAPACITY;
pub const DEFAULT_TTL: Duration = DEFAULT_TTL;
#[must_use]
pub fn new() -> Self {
Self::with_clock(
Arc::new(SystemClock),
Self::DEFAULT_TTL,
Self::DEFAULT_CAPACITY,
)
}
#[must_use]
pub fn with_clock(clock: Arc<dyn Clock>, ttl: Duration, capacity: usize) -> Self {
assert!(capacity > 0, "ReferencesCache capacity must be > 0");
let cap_hint = capacity.min(1024);
Self {
inner: Mutex::new(CacheInner {
entries: HashMap::with_capacity(cap_hint),
lru: VecDeque::with_capacity(cap_hint),
by_uri: HashMap::with_capacity(cap_hint),
}),
clock,
ttl,
capacity,
}
}
pub fn get(&self, key: &CacheKey) -> Option<Vec<Location>> {
let mut guard = self.inner.lock().expect("cache mutex poisoned");
let now = self.clock.now();
let ttl = self.ttl;
let stored_at = match guard.entries.get(key) {
Some((stored_at, _)) => *stored_at,
None => return None,
};
if now.duration_since(stored_at) > ttl {
guard.entries.remove(key);
guard.lru.retain(|k| k != key);
remove_from_by_uri(&mut guard.by_uri, key);
return None;
}
guard.lru.retain(|k| k != key);
guard.lru.push_front(key.clone());
guard.entries.get(key).map(|(_, locs)| locs.clone())
}
pub fn insert(&self, key: CacheKey, locations: Vec<Location>) {
let mut guard = self.inner.lock().expect("cache mutex poisoned");
let now = self.clock.now();
if guard.entries.contains_key(&key) {
guard.lru.retain(|k| k != &key);
} else if guard.entries.len() >= self.capacity {
if let Some(evicted) = guard.lru.pop_back() {
remove_from_by_uri(&mut guard.by_uri, &evicted);
guard.entries.remove(&evicted);
}
}
guard
.by_uri
.entry(key.uri.clone())
.or_default()
.insert(key.clone());
guard.entries.insert(key.clone(), (now, locations));
guard.lru.push_front(key);
}
pub fn invalidate_uri(&self, uri: &str) -> usize {
let mut guard = self.inner.lock().expect("cache mutex poisoned");
let keys = match guard.by_uri.remove(uri) {
Some(set) => set,
None => return 0,
};
let count = keys.len();
for key in keys {
guard.entries.remove(&key);
guard.lru.retain(|k| k != &key);
}
count
}
pub fn len(&self) -> usize {
self.inner
.lock()
.expect("cache mutex poisoned")
.entries
.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub const fn ttl(&self) -> Duration {
self.ttl
}
pub const fn capacity(&self) -> usize {
self.capacity
}
}
impl Default for ReferencesCache {
fn default() -> Self {
Self::new()
}
}
fn remove_from_by_uri(by_uri: &mut HashMap<String, HashSet<CacheKey>>, key: &CacheKey) {
let mut empty = false;
if let Some(set) = by_uri.get_mut(&key.uri) {
set.remove(key);
empty = set.is_empty();
}
if empty {
by_uri.remove(&key.uri);
}
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::{Position, Range, Uri};
use std::str::FromStr;
fn loc(line: u32) -> Location {
Location {
uri: Uri::from_str("file:///tmp/x.rs").unwrap(),
range: Range {
start: Position { line, character: 0 },
end: Position { line, character: 5 },
},
}
}
#[test]
fn new_uses_defaults() {
let c = ReferencesCache::new();
assert_eq!(c.ttl(), Duration::from_secs(300));
assert_eq!(c.capacity(), 1_000);
assert!(c.is_empty());
}
#[test]
fn insert_then_get_returns_value() {
let c = ReferencesCache::new();
let key = CacheKey::new("file:///tmp/x.rs".into(), 5, 10);
c.insert(key.clone(), vec![loc(5)]);
assert_eq!(c.len(), 1);
let got = c.get(&key).expect("entry present");
assert_eq!(got.len(), 1);
assert_eq!(got[0].range.start.line, 5);
}
#[test]
fn get_missing_returns_none() {
let c = ReferencesCache::new();
let key = CacheKey::new("file:///tmp/x.rs".into(), 5, 10);
assert!(c.get(&key).is_none());
}
#[test]
fn expired_entry_is_evicted_on_read() {
let clock = Arc::new(MockClock::new());
let c = ReferencesCache::with_clock(clock.clone(), Duration::from_secs(60), 100);
let key = CacheKey::new("file:///tmp/x.rs".into(), 5, 10);
c.insert(key.clone(), vec![loc(5)]);
assert!(c.get(&key).is_some());
clock.advance(Duration::from_secs(61));
assert!(c.get(&key).is_none(), "expired entry should be evicted");
assert_eq!(c.len(), 0, "eviction should remove from cache");
}
#[test]
fn ttl_boundary_exactly_at_ttl_is_hit() {
let clock = Arc::new(MockClock::new());
let c = ReferencesCache::with_clock(clock.clone(), Duration::from_secs(60), 100);
let key = CacheKey::new("file:///tmp/x.rs".into(), 5, 10);
c.insert(key.clone(), vec![loc(5)]);
clock.advance(Duration::from_secs(60));
assert!(c.get(&key).is_some(), "age == ttl should still be fresh");
}
#[test]
fn lru_eviction_when_capacity_exceeded() {
let clock = Arc::new(MockClock::new());
let c = ReferencesCache::with_clock(clock, Duration::from_secs(600), 2);
let k1 = CacheKey::new("file:///tmp/a.rs".into(), 1, 1);
let k2 = CacheKey::new("file:///tmp/b.rs".into(), 2, 2);
let k3 = CacheKey::new("file:///tmp/c.rs".into(), 3, 3);
c.insert(k1.clone(), vec![loc(1)]);
c.insert(k2.clone(), vec![loc(2)]);
c.insert(k3.clone(), vec![loc(3)]);
assert!(c.get(&k1).is_none(), "k1 should have been LRU-evicted");
assert!(c.get(&k2).is_some());
assert!(c.get(&k3).is_some());
assert_eq!(c.len(), 2);
}
#[test]
fn lru_refresh_on_get_prevents_eviction() {
let clock = Arc::new(MockClock::new());
let c = ReferencesCache::with_clock(clock, Duration::from_secs(600), 2);
let k1 = CacheKey::new("file:///tmp/a.rs".into(), 1, 1);
let k2 = CacheKey::new("file:///tmp/b.rs".into(), 2, 2);
let k3 = CacheKey::new("file:///tmp/c.rs".into(), 3, 3);
c.insert(k1.clone(), vec![loc(1)]);
c.insert(k2.clone(), vec![loc(2)]);
let _ = c.get(&k1);
c.insert(k3.clone(), vec![loc(3)]);
assert!(c.get(&k1).is_some(), "k1 was refreshed, should survive");
assert!(c.get(&k2).is_none(), "k2 was LRU, should be evicted");
assert!(c.get(&k3).is_some());
}
#[test]
fn invalidate_uri_removes_all_entries_for_file() {
let c = ReferencesCache::new();
c.insert(CacheKey::new("file:///tmp/x.rs".into(), 1, 1), vec![loc(1)]);
c.insert(
CacheKey::new("file:///tmp/x.rs".into(), 5, 10),
vec![loc(5)],
);
c.insert(CacheKey::new("file:///tmp/y.rs".into(), 1, 1), vec![loc(1)]);
let evicted = c.invalidate_uri("file:///tmp/x.rs");
assert_eq!(evicted, 2);
assert_eq!(c.len(), 1, "y.rs entry must survive");
}
#[test]
fn invalidate_uri_no_match_returns_zero() {
let c = ReferencesCache::new();
c.insert(CacheKey::new("file:///tmp/x.rs".into(), 1, 1), vec![loc(1)]);
assert_eq!(c.invalidate_uri("file:///tmp/other.rs"), 0);
assert_eq!(c.len(), 1);
}
#[test]
fn invalidate_uri_after_lru_evict_does_not_count_evicted_keys() {
let clock = Arc::new(MockClock::new());
let c = ReferencesCache::with_clock(clock, Duration::from_secs(600), 2);
let k1 = CacheKey::new("file:///tmp/x.rs".into(), 1, 1);
let k2 = CacheKey::new("file:///tmp/x.rs".into(), 2, 2);
c.insert(k1.clone(), vec![loc(1)]);
c.insert(k2.clone(), vec![loc(2)]);
let k3 = CacheKey::new("file:///tmp/y.rs".into(), 3, 3);
c.insert(k3.clone(), vec![loc(3)]);
assert!(c.get(&k1).is_none(), "k1 should have been LRU-evicted");
let evicted = c.invalidate_uri("file:///tmp/x.rs");
assert_eq!(evicted, 1, "only k2 should be evicted (k1 already gone)");
assert_eq!(c.len(), 1, "y.rs entry must survive");
}
#[test]
fn invalidate_uri_after_lazy_expiration_does_not_count_expired_keys() {
let clock = Arc::new(MockClock::new());
let c = ReferencesCache::with_clock(clock.clone(), Duration::from_secs(60), 100);
let k1 = CacheKey::new("file:///tmp/x.rs".into(), 1, 1);
let k2 = CacheKey::new("file:///tmp/x.rs".into(), 2, 2);
c.insert(k1.clone(), vec![loc(1)]);
c.insert(k2.clone(), vec![loc(2)]);
clock.advance(Duration::from_secs(61));
assert!(c.get(&k1).is_none(), "k1 should be expired");
let evicted = c.invalidate_uri("file:///tmp/x.rs");
assert_eq!(evicted, 1, "only k2 should be evicted (k1 already expired)");
assert_eq!(c.len(), 0);
}
#[test]
fn insert_overwrites_existing_key() {
let c = ReferencesCache::new();
let key = CacheKey::new("file:///tmp/x.rs".into(), 5, 10);
c.insert(key.clone(), vec![loc(5)]);
c.insert(key.clone(), vec![loc(5), loc(6)]);
assert_eq!(c.len(), 1, "overwrite must not grow cache");
let got = c.get(&key).expect("entry present");
assert_eq!(got.len(), 2);
}
#[test]
fn mock_clock_with_start_is_deterministic() {
let anchor = Instant::now();
let c = MockClock::with_start(anchor);
assert_eq!(c.now(), anchor);
c.advance(Duration::from_secs(10));
assert_eq!(c.now(), anchor + Duration::from_secs(10));
}
#[test]
#[should_panic(expected = "capacity must be > 0")]
fn zero_capacity_panics() {
let clock = Arc::new(SystemClock) as Arc<dyn Clock>;
let _ = ReferencesCache::with_clock(clock, Duration::from_secs(60), 0);
}
#[test]
fn system_clock_advances_monotonically() {
let c = SystemClock;
let t1 = c.now();
while c.now() == t1 {
std::hint::spin_loop();
}
assert!(c.now() > t1);
}
}