pub mod diff;
pub mod types;
use std::sync::atomic::{AtomicUsize, Ordering};
use kovan_map::HashMap;
use crate::error::Result;
pub use types::{CacheStats, ReadResult};
#[derive(Clone)]
#[allow(dead_code)]
struct CachedFile {
content: String,
content_hash: u64,
lines: usize,
tokens_estimated: usize,
}
pub struct FileCache {
files: HashMap<u64, CachedFile>,
total_tokens_saved: AtomicUsize,
}
fn hash(s: &str) -> u64 {
ahash::RandomState::with_seeds(0, 0, 0, 0).hash_one(s)
}
fn estimate_tokens(s: &str) -> usize {
(s.len() as f64 * 0.75).ceil() as usize
}
impl FileCache {
pub fn new() -> Self {
Self {
files: HashMap::new(),
total_tokens_saved: AtomicUsize::new(0),
}
}
pub fn read_file(&self, path: &str) -> Result<ReadResult> {
let content = std::fs::read_to_string(path)?;
self.process(path, content)
}
pub fn read_file_range(&self, path: &str, offset: usize, limit: usize) -> Result<ReadResult> {
let full_content = std::fs::read_to_string(path)?;
let sliced: String = full_content
.lines()
.skip(offset)
.take(limit)
.collect::<Vec<_>>()
.join("\n");
let lines = sliced.lines().count();
let tokens = estimate_tokens(&sliced);
let path_hash = hash(path);
let content_hash = hash(&full_content);
let full_lines = full_content.lines().count();
let full_tokens = estimate_tokens(&full_content);
self.files.insert(
path_hash,
CachedFile {
content: full_content,
content_hash,
lines: full_lines,
tokens_estimated: full_tokens,
},
);
Ok(ReadResult::Fresh {
content: sliced,
lines,
tokens_estimated: tokens,
})
}
pub fn stats(&self) -> CacheStats {
CacheStats {
files_tracked: self.files.len(),
tokens_saved: self.total_tokens_saved.load(Ordering::Relaxed),
}
}
pub fn clear(&self) {
self.files.clear();
self.total_tokens_saved.store(0, Ordering::Relaxed);
}
pub fn invalidate(&self, path: &str) {
let path_hash = hash(path);
self.files.remove(&path_hash);
}
fn process(&self, path: &str, content: String) -> Result<ReadResult> {
let path_hash = hash(path);
let content_hash = hash(&content);
let lines = content.lines().count();
let tokens = estimate_tokens(&content);
if let Some(cached) = self.files.get(&path_hash) {
if cached.content_hash == content_hash {
self.total_tokens_saved.fetch_add(tokens, Ordering::Relaxed);
return Ok(ReadResult::Unchanged {
path: path.to_string(),
lines: cached.lines,
tokens_saved: tokens,
});
}
let (diff_text, lines_changed) = diff::unified_diff(path, &cached.content, &content);
let diff_tokens = estimate_tokens(&diff_text);
let saved = tokens.saturating_sub(diff_tokens);
self.total_tokens_saved.fetch_add(saved, Ordering::Relaxed);
self.files.insert(
path_hash,
CachedFile {
content,
content_hash,
lines,
tokens_estimated: tokens,
},
);
return Ok(ReadResult::Modified {
diff: diff_text,
lines_changed,
tokens_saved: saved,
});
}
self.files.insert(
path_hash,
CachedFile {
content: content.clone(),
content_hash,
lines,
tokens_estimated: tokens,
},
);
Ok(ReadResult::Fresh {
content,
lines,
tokens_estimated: tokens,
})
}
}
impl Default for FileCache {
fn default() -> Self {
Self::new()
}
}