1use std::path::{Component, Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::settings::Settings;
8
9pub const DEFAULT_CAP: usize = 10;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct RecentFiles {
15 paths: Vec<PathBuf>,
16 cap: usize,
17}
18
19impl Default for RecentFiles {
20 fn default() -> Self {
21 RecentFiles::new(DEFAULT_CAP)
22 }
23}
24
25impl RecentFiles {
26 pub fn new(cap: usize) -> RecentFiles {
28 RecentFiles { paths: Vec::new(), cap: cap.max(1) }
29 }
30
31 pub fn push(&mut self, path: impl AsRef<Path>) {
34 let p = normalize(path.as_ref());
35 if p.as_os_str().is_empty() {
36 return;
37 }
38 self.paths.retain(|q| *q != p);
39 self.paths.insert(0, p);
40 self.paths.truncate(self.cap);
41 }
42
43 pub fn remove(&mut self, path: impl AsRef<Path>) -> bool {
45 let p = normalize(path.as_ref());
46 let before = self.paths.len();
47 self.paths.retain(|q| *q != p);
48 self.paths.len() != before
49 }
50
51 pub fn prune(&mut self, exists: impl Fn(&Path) -> bool) -> usize {
54 let before = self.paths.len();
55 self.paths.retain(|p| exists(p));
56 before - self.paths.len()
57 }
58
59 pub fn clear(&mut self) {
60 self.paths.clear();
61 }
62
63 pub fn paths(&self) -> &[PathBuf] {
64 &self.paths
65 }
66
67 pub fn cap(&self) -> usize {
68 self.cap
69 }
70
71 pub fn set_cap(&mut self, cap: usize) {
73 self.cap = cap.max(1);
74 self.paths.truncate(self.cap);
75 }
76
77 pub fn load(settings: &Settings, key: &str, cap: usize) -> RecentFiles {
80 let mut r = RecentFiles::new(cap);
81 let items: Vec<serde_json::Value> = settings.get(key).unwrap_or_default();
82 for s in items.iter().rev().filter_map(|v| v.as_str()) {
83 r.push(s);
84 }
85 r
86 }
87
88 pub fn store(&self, settings: &mut Settings, key: &str) {
90 let v: Vec<String> = self.paths.iter().map(|p| p.to_string_lossy().into_owned()).collect();
91 settings.set_value(key, serde_json::Value::from(v));
92 }
93}
94
95fn normalize(p: &Path) -> PathBuf {
98 p.components().filter(|c| !matches!(c, Component::CurDir)).collect()
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::backend::{ConfigStore, MemoryConfig};
105 use std::sync::Arc;
106
107 #[test]
108 fn mru_order_dedupe_and_cap() {
109 let mut r = RecentFiles::new(3);
110 r.push("/a");
111 r.push("/b");
112 r.push("/c");
113 r.push("/a/"); assert_eq!(r.paths(), [PathBuf::from("/a"), "/c".into(), "/b".into()]);
115 r.push("/d");
116 assert_eq!(r.paths(), [PathBuf::from("/d"), "/a".into(), "/c".into()]);
117 r.push("/./c");
118 assert_eq!(r.paths()[0], PathBuf::from("/c"));
119 r.push("");
120 assert_eq!(r.paths().len(), 3);
121 assert!(r.remove("/a"));
122 assert!(!r.remove("/a"));
123 r.set_cap(1);
124 assert_eq!(r.paths(), [PathBuf::from("/c")]);
125 assert_eq!(RecentFiles::new(0).cap(), 1);
126 }
127
128 #[test]
129 fn prune_missing() {
130 let mut r = RecentFiles::new(5);
131 for p in ["/keep1", "/gone", "/keep2"] {
132 r.push(p);
133 }
134 assert_eq!(r.prune(|p| p.to_string_lossy().contains("keep")), 1);
135 assert_eq!(r.paths(), [PathBuf::from("/keep2"), "/keep1".into()]);
136 }
137
138 #[test]
139 fn round_trip_through_settings_and_hostile_values() {
140 let store: Arc<dyn ConfigStore> = Arc::new(MemoryConfig::new());
141 let mut s = Settings::load(store, "s.json", &[]);
142 let mut r = RecentFiles::new(3);
143 r.push("/x");
144 r.push("/y");
145 r.store(&mut s, "recent");
146 assert_eq!(RecentFiles::load(&s, "recent", 3), r);
147 s.set_value("recent", serde_json::json!(["/1", 5, null, "/2", "/3", "/4", "/1"]));
148 let back = RecentFiles::load(&s, "recent", 3);
149 assert_eq!(back.paths(), [PathBuf::from("/1"), "/2".into(), "/3".into()]);
150 s.set_value("recent", serde_json::json!({"not": "a list"}));
151 assert!(RecentFiles::load(&s, "recent", 3).paths().is_empty());
152 }
153}