cersei_tools/
file_history.rs1use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Default)]
11pub struct FileHistory {
12 entries: HashMap<PathBuf, FileAccess>,
13}
14
15#[derive(Debug, Clone)]
16pub struct FileAccess {
17 pub path: PathBuf,
18 pub read_count: u32,
19 pub write_count: u32,
20 pub edit_count: u32,
21 pub last_accessed: u64,
22}
23
24impl FileHistory {
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn record_read(&mut self, path: &PathBuf) {
30 let entry = self
31 .entries
32 .entry(path.clone())
33 .or_insert_with(|| FileAccess {
34 path: path.clone(),
35 read_count: 0,
36 write_count: 0,
37 edit_count: 0,
38 last_accessed: 0,
39 });
40 entry.read_count += 1;
41 entry.last_accessed = now_secs();
42 }
43
44 pub fn record_write(&mut self, path: &PathBuf) {
45 let entry = self
46 .entries
47 .entry(path.clone())
48 .or_insert_with(|| FileAccess {
49 path: path.clone(),
50 read_count: 0,
51 write_count: 0,
52 edit_count: 0,
53 last_accessed: 0,
54 });
55 entry.write_count += 1;
56 entry.last_accessed = now_secs();
57 }
58
59 pub fn record_edit(&mut self, path: &PathBuf) {
60 let entry = self
61 .entries
62 .entry(path.clone())
63 .or_insert_with(|| FileAccess {
64 path: path.clone(),
65 read_count: 0,
66 write_count: 0,
67 edit_count: 0,
68 last_accessed: 0,
69 });
70 entry.edit_count += 1;
71 entry.last_accessed = now_secs();
72 }
73
74 pub fn all_files(&self) -> Vec<&FileAccess> {
76 let mut files: Vec<_> = self.entries.values().collect();
77 files.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
78 files
79 }
80
81 pub fn modified_files(&self) -> Vec<&FileAccess> {
83 let mut files: Vec<_> = self
84 .entries
85 .values()
86 .filter(|f| f.write_count > 0 || f.edit_count > 0)
87 .collect();
88 files.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
89 files
90 }
91
92 pub fn build_context(&self) -> Option<String> {
94 let modified = self.modified_files();
95 if modified.is_empty() {
96 return None;
97 }
98
99 let lines: Vec<String> = modified
100 .iter()
101 .take(20)
102 .map(|f| {
103 let ops = format!(
104 "{}{}{}",
105 if f.read_count > 0 {
106 format!("r{} ", f.read_count)
107 } else {
108 String::new()
109 },
110 if f.write_count > 0 {
111 format!("w{} ", f.write_count)
112 } else {
113 String::new()
114 },
115 if f.edit_count > 0 {
116 format!("e{}", f.edit_count)
117 } else {
118 String::new()
119 },
120 );
121 format!("- {} ({})", f.path.display(), ops.trim())
122 })
123 .collect();
124
125 Some(format!(
126 "Files modified this session:\n{}",
127 lines.join("\n")
128 ))
129 }
130
131 pub fn file_count(&self) -> usize {
132 self.entries.len()
133 }
134
135 pub fn clear(&mut self) {
136 self.entries.clear();
137 }
138}
139
140fn now_secs() -> u64 {
141 SystemTime::now()
142 .duration_since(UNIX_EPOCH)
143 .map(|d| d.as_secs())
144 .unwrap_or(0)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_file_history() {
153 let mut history = FileHistory::new();
154 let path1 = PathBuf::from("src/main.rs");
155 let path2 = PathBuf::from("Cargo.toml");
156
157 history.record_read(&path1);
158 history.record_read(&path1);
159 history.record_edit(&path1);
160 history.record_write(&path2);
161
162 assert_eq!(history.file_count(), 2);
163 assert_eq!(history.all_files().len(), 2);
164 assert_eq!(history.modified_files().len(), 2);
165
166 let main = history.entries.get(&path1).unwrap();
167 assert_eq!(main.read_count, 2);
168 assert_eq!(main.edit_count, 1);
169 }
170
171 #[test]
172 fn test_build_context() {
173 let mut history = FileHistory::new();
174 history.record_edit(&PathBuf::from("src/lib.rs"));
175 history.record_write(&PathBuf::from("README.md"));
176
177 let ctx = history.build_context();
178 assert!(ctx.is_some());
179 assert!(ctx.unwrap().contains("src/lib.rs"));
180 }
181
182 #[test]
183 fn test_empty_context() {
184 let history = FileHistory::new();
185 assert!(history.build_context().is_none());
186
187 let mut history = FileHistory::new();
189 history.record_read(&PathBuf::from("file.txt"));
190 assert!(history.build_context().is_none());
191 }
192}