use std::path::{Component, Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::settings::Settings;
pub const DEFAULT_CAP: usize = 10;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecentFiles {
paths: Vec<PathBuf>,
cap: usize,
}
impl Default for RecentFiles {
fn default() -> Self {
RecentFiles::new(DEFAULT_CAP)
}
}
impl RecentFiles {
pub fn new(cap: usize) -> RecentFiles {
RecentFiles { paths: Vec::new(), cap: cap.max(1) }
}
pub fn push(&mut self, path: impl AsRef<Path>) {
let p = normalize(path.as_ref());
if p.as_os_str().is_empty() {
return;
}
self.paths.retain(|q| *q != p);
self.paths.insert(0, p);
self.paths.truncate(self.cap);
}
pub fn remove(&mut self, path: impl AsRef<Path>) -> bool {
let p = normalize(path.as_ref());
let before = self.paths.len();
self.paths.retain(|q| *q != p);
self.paths.len() != before
}
pub fn prune(&mut self, exists: impl Fn(&Path) -> bool) -> usize {
let before = self.paths.len();
self.paths.retain(|p| exists(p));
before - self.paths.len()
}
pub fn clear(&mut self) {
self.paths.clear();
}
pub fn paths(&self) -> &[PathBuf] {
&self.paths
}
pub fn cap(&self) -> usize {
self.cap
}
pub fn set_cap(&mut self, cap: usize) {
self.cap = cap.max(1);
self.paths.truncate(self.cap);
}
pub fn load(settings: &Settings, key: &str, cap: usize) -> RecentFiles {
let mut r = RecentFiles::new(cap);
let items: Vec<serde_json::Value> = settings.get(key).unwrap_or_default();
for s in items.iter().rev().filter_map(|v| v.as_str()) {
r.push(s);
}
r
}
pub fn store(&self, settings: &mut Settings, key: &str) {
let v: Vec<String> = self.paths.iter().map(|p| p.to_string_lossy().into_owned()).collect();
settings.set_value(key, serde_json::Value::from(v));
}
}
fn normalize(p: &Path) -> PathBuf {
p.components().filter(|c| !matches!(c, Component::CurDir)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::{ConfigStore, MemoryConfig};
use std::sync::Arc;
#[test]
fn mru_order_dedupe_and_cap() {
let mut r = RecentFiles::new(3);
r.push("/a");
r.push("/b");
r.push("/c");
r.push("/a/"); assert_eq!(r.paths(), [PathBuf::from("/a"), "/c".into(), "/b".into()]);
r.push("/d");
assert_eq!(r.paths(), [PathBuf::from("/d"), "/a".into(), "/c".into()]);
r.push("/./c");
assert_eq!(r.paths()[0], PathBuf::from("/c"));
r.push("");
assert_eq!(r.paths().len(), 3);
assert!(r.remove("/a"));
assert!(!r.remove("/a"));
r.set_cap(1);
assert_eq!(r.paths(), [PathBuf::from("/c")]);
assert_eq!(RecentFiles::new(0).cap(), 1);
}
#[test]
fn prune_missing() {
let mut r = RecentFiles::new(5);
for p in ["/keep1", "/gone", "/keep2"] {
r.push(p);
}
assert_eq!(r.prune(|p| p.to_string_lossy().contains("keep")), 1);
assert_eq!(r.paths(), [PathBuf::from("/keep2"), "/keep1".into()]);
}
#[test]
fn round_trip_through_settings_and_hostile_values() {
let store: Arc<dyn ConfigStore> = Arc::new(MemoryConfig::new());
let mut s = Settings::load(store, "s.json", &[]);
let mut r = RecentFiles::new(3);
r.push("/x");
r.push("/y");
r.store(&mut s, "recent");
assert_eq!(RecentFiles::load(&s, "recent", 3), r);
s.set_value("recent", serde_json::json!(["/1", 5, null, "/2", "/3", "/4", "/1"]));
let back = RecentFiles::load(&s, "recent", 3);
assert_eq!(back.paths(), [PathBuf::from("/1"), "/2".into(), "/3".into()]);
s.set_value("recent", serde_json::json!({"not": "a list"}));
assert!(RecentFiles::load(&s, "recent", 3).paths().is_empty());
}
}