use std::collections::HashMap;
use std::path::{Path, PathBuf};
use scryer_engine::hasher::hash_bytes;
const LINE_CHARS: usize = 160;
#[derive(Default)]
pub(crate) struct LineCache {
files: HashMap<PathBuf, Option<Vec<String>>>,
stale: usize,
}
impl LineCache {
pub(crate) fn line(
&mut self,
root: &Path,
rel: &str,
expected_hash: &str,
line: u32,
) -> Option<String> {
let path = root.join(rel);
if !self.files.contains_key(&path) {
let lines = std::fs::read(&path).ok().and_then(|bytes| {
if !expected_hash.is_empty() && hash_bytes(&bytes) != expected_hash {
self.stale += 1;
return None;
}
Some(
String::from_utf8_lossy(&bytes)
.lines()
.map(str::to_string)
.collect(),
)
});
self.files.insert(path.clone(), lines);
}
let text = self.files[&path]
.as_ref()?
.get(line.checked_sub(1)? as usize)?;
Some(cut(text.trim()))
}
pub(crate) fn range(
&mut self,
root: &Path,
rel: &str,
expected_hash: &str,
start: u32,
end: u32,
max_lines: usize,
) -> Option<(String, usize)> {
self.line(root, rel, expected_hash, start.max(1))?;
let lines = self.files[&root.join(rel)].as_ref()?;
let from = start.saturating_sub(1) as usize;
let to = (end as usize).min(lines.len());
let all = lines.get(from..to)?;
let text = all
.iter()
.take(max_lines)
.map(|l| cut(l.trim_end()))
.collect::<Vec<_>>()
.join("\n");
Some((text, all.len()))
}
pub(crate) fn stale_files(&self) -> usize {
self.stale
}
}
fn cut(text: &str) -> String {
if text.chars().count() <= LINE_CHARS {
return text.to_string();
}
let mut out: String = text.chars().take(LINE_CHARS - 1).collect();
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quotes_the_trimmed_line_and_skips_changed_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.rs"), "fn a() {}\n let x = 1;\n").unwrap();
let hash = hash_bytes(&std::fs::read(dir.path().join("a.rs")).unwrap());
let mut cache = LineCache::default();
assert_eq!(
cache.line(dir.path(), "a.rs", &hash, 2).as_deref(),
Some("let x = 1;")
);
assert_eq!(cache.line(dir.path(), "a.rs", &hash, 9), None);
assert_eq!(cache.line(dir.path(), "a.rs", &hash, 0), None);
let mut stale = LineCache::default();
assert_eq!(stale.line(dir.path(), "a.rs", "not-the-hash", 1), None);
assert_eq!(stale.stale_files(), 1);
assert_eq!(stale.line(dir.path(), "missing.rs", &hash, 1), None);
}
#[test]
fn long_lines_are_cut_with_an_ellipsis() {
let long = "x".repeat(400);
let cut = cut(&long);
assert_eq!(cut.chars().count(), LINE_CHARS);
assert!(cut.ends_with('…'));
}
}