use std::path::{Path, PathBuf};
use super::{Editor, Snapshot, UNDO_LIMIT};
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub(crate) struct PersistedHistory {
text_hash: u64,
undo: Vec<Snapshot>,
redo: Vec<Snapshot>,
}
pub(crate) const PERSISTED_UNDO_LIMIT: usize = 100;
pub fn undo_path_for(workspace: &Path, file_path: &Path) -> PathBuf {
let key = file_path.to_string_lossy();
let hash = fnv1a_64(&key);
workspace
.join(".mnml")
.join("undo")
.join(format!("{hash:016x}.json"))
}
pub fn save_history_to(editor: &Editor, path: &Path) -> bool {
let Some(parent) = path.parent() else {
return false;
};
if std::fs::create_dir_all(parent).is_err() {
return false;
}
let snapshot = editor.snapshot_history();
let Ok(json) = serde_json::to_string(&snapshot) else {
return false;
};
std::fs::write(path, json).is_ok()
}
pub fn load_history_from(editor: &mut Editor, path: &Path) -> bool {
let Ok(text) = std::fs::read_to_string(path) else {
return false;
};
let Ok(h) = serde_json::from_str::<PersistedHistory>(&text) else {
return false;
};
editor.restore_history(h)
}
pub(crate) fn fnv1a_64(s: &str) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut h = FNV_OFFSET;
for b in s.bytes() {
h ^= b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h
}
impl Editor {
pub(crate) fn snapshot_history(&self) -> PersistedHistory {
let take_tail = |v: &[Snapshot]| -> Vec<Snapshot> {
let n = v.len();
let start = n.saturating_sub(PERSISTED_UNDO_LIMIT);
v[start..].to_vec()
};
PersistedHistory {
text_hash: fnv1a_64(&self.text),
undo: take_tail(&self.undo),
redo: take_tail(&self.redo),
}
}
pub(crate) fn restore_history(&mut self, h: PersistedHistory) -> bool {
if h.text_hash != fnv1a_64(&self.text) {
return false;
}
self.undo = h.undo;
self.redo = h.redo;
let trim = |v: &mut Vec<Snapshot>| {
if v.len() > UNDO_LIMIT {
let drop = v.len() - UNDO_LIMIT;
v.drain(..drop);
}
};
trim(&mut self.undo);
trim(&mut self.redo);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fnv1a_64_is_stable() {
assert_eq!(fnv1a_64("hello"), fnv1a_64("hello"));
assert_ne!(fnv1a_64("hello"), fnv1a_64("hellp"));
}
#[test]
fn undo_path_includes_hex_hash() {
let p = undo_path_for(Path::new("/ws"), Path::new("/ws/src/main.rs"));
let name = p.file_name().unwrap().to_string_lossy().to_string();
assert!(name.ends_with(".json"));
assert_eq!(name.len(), 16 + ".json".len());
}
}