use std::collections::{HashMap, HashSet};
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard};
use crate::document::Document;
pub fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
const BUDGET: usize = 32 * 1024 * 1024;
const MAX_DOCUMENT: usize = 1024 * 1024;
#[derive(Debug, Default)]
pub(crate) struct DirNames {
exact: HashSet<OsString>,
folded: HashMap<OsString, OsString>,
}
impl DirNames {
pub(crate) fn index(entries: &[crate::fs::DirEntry]) -> Self {
let mut names = Self::default();
for entry in entries {
let Some(name) = entry.file_name() else {
continue;
};
names
.folded
.insert(name.to_ascii_lowercase(), name.to_os_string());
names.exact.insert(name.to_os_string());
}
names
}
pub(crate) fn holds(&self, name: &OsStr) -> bool {
self.exact.contains(name)
}
pub(crate) fn case_variant(&self, name: &OsStr) -> Option<&OsStr> {
self.folded
.get(&name.to_ascii_lowercase())
.map(OsString::as_os_str)
}
fn weight(&self) -> usize {
let names = self.exact.iter().map(|n| n.len()).sum::<usize>();
let folded = self
.folded
.iter()
.map(|(k, v)| k.len() + v.len())
.sum::<usize>();
names + folded
}
}
#[derive(Debug, Default)]
pub struct ReadMemo {
depth: usize,
docs: HashMap<PathBuf, (String, Document)>,
dirs: HashMap<PathBuf, Arc<DirNames>>,
bytes: usize,
}
impl ReadMemo {
pub(crate) fn enter(&mut self) {
self.depth += 1;
}
pub(crate) fn leave(&mut self) {
self.depth = self.depth.saturating_sub(1);
if self.depth == 0 {
self.clear();
}
}
pub(crate) fn get(&self, path: &Path) -> Option<(String, Document)> {
if self.depth == 0 {
return None;
}
self.docs.get(path).cloned()
}
pub(crate) fn remember(&mut self, path: &Path, text: &str, doc: &Document) {
if self.depth == 0 || text.len() > MAX_DOCUMENT || self.bytes + text.len() > BUDGET {
return;
}
self.bytes += text.len();
self.docs
.insert(path.to_path_buf(), (text.to_string(), doc.clone()));
}
pub(crate) fn dir(&self, path: &Path) -> Option<Arc<DirNames>> {
if self.depth == 0 {
return None;
}
self.dirs.get(path).cloned()
}
pub(crate) fn remember_dir(&mut self, path: &Path, names: Arc<DirNames>) {
let weight = names.weight();
if self.depth == 0 || self.bytes + weight > BUDGET {
return;
}
self.bytes += weight;
self.dirs.insert(path.to_path_buf(), names);
}
pub fn forget(&mut self, path: &Path) {
if let Some((text, _)) = self.docs.remove(path) {
self.bytes -= text.len();
}
if let Some(parent) = path.parent()
&& let Some(names) = self.dirs.remove(parent)
{
self.bytes -= names.weight();
}
}
pub(crate) fn clear(&mut self) {
self.docs.clear();
self.dirs.clear();
self.bytes = 0;
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.docs.len()
}
#[cfg(test)]
pub(crate) fn dirs_len(&self) -> usize {
self.dirs.len()
}
}
#[must_use = "a read scope ends the moment its guard is dropped"]
pub struct ReadScope(Arc<Mutex<ReadMemo>>);
impl ReadScope {
pub(crate) fn open(memo: &Arc<Mutex<ReadMemo>>) -> Self {
lock(memo).enter();
ReadScope(Arc::clone(memo))
}
}
impl Drop for ReadScope {
fn drop(&mut self) {
lock(&self.0).leave();
}
}
impl std::fmt::Debug for ReadScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReadScope")
.field("depth", &lock(&self.0).depth)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(text: &str) -> Document {
Document::parse("a.md", text).unwrap()
}
#[test]
fn nothing_is_remembered_outside_a_scope() {
let mut memo = ReadMemo::default();
memo.remember(Path::new("a.md"), "hello", &doc("hello"));
assert_eq!(memo.len(), 0);
assert!(memo.get(Path::new("a.md")).is_none());
}
#[test]
fn a_scope_remembers_and_its_exit_forgets() {
let mut memo = ReadMemo::default();
memo.enter();
memo.remember(Path::new("a.md"), "hello", &doc("hello"));
assert_eq!(
memo.get(Path::new("a.md")).map(|(text, _)| text),
Some("hello".to_string())
);
memo.leave();
assert_eq!(memo.len(), 0);
}
#[test]
fn an_inner_scope_does_not_end_the_outer_one() {
let mut memo = ReadMemo::default();
memo.enter();
memo.remember(Path::new("a.md"), "hello", &doc("hello"));
memo.enter();
memo.leave();
assert!(
memo.get(Path::new("a.md")).is_some(),
"an inner scope's exit dropped the outer scope's memo"
);
memo.leave();
assert_eq!(memo.len(), 0);
}
#[test]
fn a_write_forgets_the_document_it_wrote() {
let mut memo = ReadMemo::default();
memo.enter();
memo.remember(Path::new("a.md"), "hello", &doc("hello"));
memo.forget(Path::new("a.md"));
assert!(memo.get(Path::new("a.md")).is_none());
assert_eq!(memo.len(), 0);
}
fn names(entries: &[&str]) -> Arc<DirNames> {
let entries: Vec<crate::fs::DirEntry> = entries
.iter()
.map(|n| crate::fs::DirEntry::new(*n, crate::fs::FileType::FILE))
.collect();
Arc::new(DirNames::index(&entries))
}
#[test]
fn a_listing_answers_an_exact_name_and_a_case_variant_apart() {
let names = names(&["Notes.md", "photo.jpg"]);
assert!(names.holds(OsStr::new("Notes.md")));
assert!(!names.holds(OsStr::new("notes.md")));
assert_eq!(
names.case_variant(OsStr::new("notes.md")),
Some(OsStr::new("Notes.md"))
);
assert_eq!(names.case_variant(OsStr::new("gone.md")), None);
}
#[test]
fn an_exact_name_wins_over_a_case_variant_of_itself() {
let names = names(&["notes.md", "Notes.md"]);
assert!(names.holds(OsStr::new("notes.md")));
assert!(names.holds(OsStr::new("Notes.md")));
}
#[test]
fn a_scope_remembers_a_directory_and_its_exit_forgets() {
let mut memo = ReadMemo::default();
memo.remember_dir(Path::new("notes"), names(&["a.md"]));
assert_eq!(memo.dirs_len(), 0, "nothing is remembered outside a scope");
memo.enter();
memo.remember_dir(Path::new("notes"), names(&["a.md"]));
assert!(memo.dir(Path::new("notes")).is_some());
memo.leave();
assert_eq!(memo.dirs_len(), 0);
}
#[test]
fn forgetting_a_written_document_forgets_its_parent_listing() {
let mut memo = ReadMemo::default();
memo.enter();
memo.remember_dir(Path::new("notes"), names(&["a.md"]));
memo.remember_dir(Path::new("other"), names(&["b.md"]));
memo.forget(Path::new("notes/new.md"));
assert!(
memo.dir(Path::new("notes")).is_none(),
"the directory the write lands in still answers from before it"
);
assert!(
memo.dir(Path::new("other")).is_some(),
"an unrelated directory was dropped"
);
}
#[test]
fn forgetting_a_root_document_forgets_the_root_listing() {
let mut memo = ReadMemo::default();
memo.enter();
memo.remember_dir(Path::new(""), names(&["index.md"]));
memo.forget(Path::new("new.md"));
assert!(memo.dir(Path::new("")).is_none());
}
#[test]
fn an_oversized_document_is_not_remembered() {
let mut memo = ReadMemo::default();
memo.enter();
let huge = "x".repeat(MAX_DOCUMENT + 1);
memo.remember(Path::new("big.md"), &huge, &doc("body"));
assert_eq!(memo.len(), 0);
}
}