use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
static OVERLAY: Mutex<Option<HashMap<PathBuf, String>>> = Mutex::new(None);
fn map() -> MutexGuard<'static, Option<HashMap<PathBuf, String>>> {
OVERLAY.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn set(path: PathBuf, text: String) {
map().get_or_insert_with(HashMap::new).insert(path, text);
}
pub fn clear(path: &Path) {
if let Some(m) = map().as_mut() {
m.remove(path);
}
}
pub fn read(path: &Path) -> std::io::Result<String> {
if let Some(t) = map().as_ref().and_then(|m| m.get(path)) {
return Ok(t.clone());
}
let bytes = std::fs::read(path)?;
String::from_utf8(bytes).map_err(|e| {
let at = e.utf8_error().valid_up_to();
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{}: {}", path.display(), utf8_message(&e.as_bytes()[..at])),
)
})
}
pub fn utf8_message(valid: &[u8]) -> String {
let line = valid.iter().filter(|b| **b == b'\n').count() + 1;
format!("{UTF8} at line {line} (byte {})", valid.len())
}
pub const UTF8: &str = "not valid UTF-8";
pub fn utf8_line(msg: &str) -> Option<usize> {
msg.split_once(" at line ")?.1.split(' ').next()?.parse().ok()
}
pub fn covers(path: &Path) -> bool {
map().as_ref().is_some_and(|m| m.contains_key(path))
}
pub fn unsaved_under(dir: &Path) -> Vec<PathBuf> {
map()
.as_ref()
.map(|m| m.keys().filter(|p| p.starts_with(dir) && !p.exists()).cloned().collect())
.unwrap_or_default()
}
pub fn stamp() -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
if let Some(m) = map().as_ref() {
let mut v: Vec<_> = m.iter().collect();
v.sort();
v.hash(&mut h);
}
h.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_read_clear() {
let p = PathBuf::from("/nonexistent/overlay-test/a.md");
set(p.clone(), "x".into());
assert_eq!(read(&p).unwrap(), "x");
assert!(covers(&p));
assert_eq!(unsaved_under(Path::new("/nonexistent/overlay-test")), vec![p.clone()]);
clear(&p);
assert!(read(&p).is_err());
}
}