1pub mod edit;
11pub mod read;
12pub mod write;
13
14use std::collections::HashMap;
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::time::SystemTime;
18
19use serde::{Deserialize, Serialize};
20use std::sync::RwLock;
21
22pub use edit::EditTool;
24pub use read::ReadTool;
25pub use write::WriteTool;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct FileReadRecord {
35 pub path: PathBuf,
37
38 pub read_at: SystemTime,
40
41 pub content_hash: String,
43
44 pub mtime: Option<SystemTime>,
46
47 pub size: u64,
49
50 pub line_count: Option<usize>,
52}
53
54impl FileReadRecord {
55 pub fn new(path: PathBuf, content_hash: String, size: u64) -> Self {
57 Self {
58 path,
59 read_at: SystemTime::now(),
60 content_hash,
61 mtime: None,
62 size,
63 line_count: None,
64 }
65 }
66
67 pub fn with_mtime(mut self, mtime: SystemTime) -> Self {
69 self.mtime = Some(mtime);
70 self
71 }
72
73 pub fn with_line_count(mut self, line_count: usize) -> Self {
75 self.line_count = Some(line_count);
76 self
77 }
78
79 pub fn is_modified(&self, current_mtime: SystemTime) -> bool {
81 match self.mtime {
82 Some(recorded_mtime) => current_mtime != recorded_mtime,
83 None => false, }
85 }
86}
87
88#[derive(Debug, Default)]
96pub struct FileReadHistory {
97 records: HashMap<PathBuf, FileReadRecord>,
99}
100
101impl FileReadHistory {
102 pub fn new() -> Self {
104 Self {
105 records: HashMap::new(),
106 }
107 }
108
109 pub fn record_read(&mut self, record: FileReadRecord) {
111 let path = record.path.clone();
112 self.records.insert(path, record);
113 }
114
115 pub fn has_read(&self, path: &PathBuf) -> bool {
117 self.records.contains_key(path)
118 }
119
120 pub fn get_record(&self, path: &PathBuf) -> Option<&FileReadRecord> {
122 self.records.get(path)
123 }
124
125 pub fn remove_record(&mut self, path: &PathBuf) -> Option<FileReadRecord> {
127 self.records.remove(path)
128 }
129
130 pub fn clear(&mut self) {
132 self.records.clear();
133 }
134
135 pub fn len(&self) -> usize {
137 self.records.len()
138 }
139
140 pub fn is_empty(&self) -> bool {
142 self.records.is_empty()
143 }
144
145 pub fn tracked_files(&self) -> Vec<&PathBuf> {
147 self.records.keys().collect()
148 }
149
150 pub fn is_file_modified(&self, path: &PathBuf, current_mtime: SystemTime) -> Option<bool> {
157 self.records
158 .get(path)
159 .map(|record| record.is_modified(current_mtime))
160 }
161}
162
163pub type SharedFileReadHistory = Arc<RwLock<FileReadHistory>>;
165
166pub fn create_shared_history() -> SharedFileReadHistory {
168 Arc::new(RwLock::new(FileReadHistory::new()))
169}
170
171pub fn compute_content_hash(content: &[u8]) -> String {
173 use std::collections::hash_map::DefaultHasher;
174 use std::hash::{Hash, Hasher};
175
176 let mut hasher = DefaultHasher::new();
177 content.hash(&mut hasher);
178 format!("{:016x}", hasher.finish())
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_file_read_record_new() {
187 let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100);
188
189 assert_eq!(record.path, PathBuf::from("/tmp/test.txt"));
190 assert_eq!(record.content_hash, "abc123");
191 assert_eq!(record.size, 100);
192 assert!(record.mtime.is_none());
193 assert!(record.line_count.is_none());
194 }
195
196 #[test]
197 fn test_file_read_record_with_mtime() {
198 let mtime = SystemTime::now();
199 let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100)
200 .with_mtime(mtime);
201
202 assert_eq!(record.mtime, Some(mtime));
203 }
204
205 #[test]
206 fn test_file_read_record_with_line_count() {
207 let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100)
208 .with_line_count(50);
209
210 assert_eq!(record.line_count, Some(50));
211 }
212
213 #[test]
214 fn test_file_read_record_is_modified() {
215 let mtime = SystemTime::now();
216 let record = FileReadRecord::new(PathBuf::from("/tmp/test.txt"), "abc123".to_string(), 100)
217 .with_mtime(mtime);
218
219 assert!(!record.is_modified(mtime));
221
222 let new_mtime = mtime + std::time::Duration::from_secs(1);
224 assert!(record.is_modified(new_mtime));
225 }
226
227 #[test]
228 fn test_file_read_history_new() {
229 let history = FileReadHistory::new();
230 assert!(history.is_empty());
231 assert_eq!(history.len(), 0);
232 }
233
234 #[test]
235 fn test_file_read_history_record_read() {
236 let mut history = FileReadHistory::new();
237 let path = PathBuf::from("/tmp/test.txt");
238 let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100);
239
240 history.record_read(record);
241
242 assert!(history.has_read(&path));
243 assert_eq!(history.len(), 1);
244 }
245
246 #[test]
247 fn test_file_read_history_get_record() {
248 let mut history = FileReadHistory::new();
249 let path = PathBuf::from("/tmp/test.txt");
250 let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100);
251
252 history.record_read(record);
253
254 let retrieved = history.get_record(&path);
255 assert!(retrieved.is_some());
256 assert_eq!(retrieved.unwrap().content_hash, "abc123");
257 }
258
259 #[test]
260 fn test_file_read_history_remove_record() {
261 let mut history = FileReadHistory::new();
262 let path = PathBuf::from("/tmp/test.txt");
263 let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100);
264
265 history.record_read(record);
266 assert!(history.has_read(&path));
267
268 let removed = history.remove_record(&path);
269 assert!(removed.is_some());
270 assert!(!history.has_read(&path));
271 }
272
273 #[test]
274 fn test_file_read_history_clear() {
275 let mut history = FileReadHistory::new();
276 history.record_read(FileReadRecord::new(
277 PathBuf::from("/tmp/test1.txt"),
278 "abc".to_string(),
279 100,
280 ));
281 history.record_read(FileReadRecord::new(
282 PathBuf::from("/tmp/test2.txt"),
283 "def".to_string(),
284 200,
285 ));
286
287 assert_eq!(history.len(), 2);
288
289 history.clear();
290 assert!(history.is_empty());
291 }
292
293 #[test]
294 fn test_file_read_history_tracked_files() {
295 let mut history = FileReadHistory::new();
296 let path1 = PathBuf::from("/tmp/test1.txt");
297 let path2 = PathBuf::from("/tmp/test2.txt");
298
299 history.record_read(FileReadRecord::new(path1.clone(), "abc".to_string(), 100));
300 history.record_read(FileReadRecord::new(path2.clone(), "def".to_string(), 200));
301
302 let tracked = history.tracked_files();
303 assert_eq!(tracked.len(), 2);
304 assert!(tracked.contains(&&path1));
305 assert!(tracked.contains(&&path2));
306 }
307
308 #[test]
309 fn test_file_read_history_is_file_modified() {
310 let mut history = FileReadHistory::new();
311 let path = PathBuf::from("/tmp/test.txt");
312 let mtime = SystemTime::now();
313 let record = FileReadRecord::new(path.clone(), "abc123".to_string(), 100).with_mtime(mtime);
314
315 history.record_read(record);
316
317 assert_eq!(history.is_file_modified(&path, mtime), Some(false));
319
320 let new_mtime = mtime + std::time::Duration::from_secs(1);
322 assert_eq!(history.is_file_modified(&path, new_mtime), Some(true));
323
324 let unknown_path = PathBuf::from("/tmp/unknown.txt");
326 assert_eq!(history.is_file_modified(&unknown_path, mtime), None);
327 }
328
329 #[test]
330 fn test_compute_content_hash() {
331 let content1 = b"Hello, World!";
332 let content2 = b"Hello, World!";
333 let content3 = b"Different content";
334
335 let hash1 = compute_content_hash(content1);
336 let hash2 = compute_content_hash(content2);
337 let hash3 = compute_content_hash(content3);
338
339 assert_eq!(hash1, hash2);
341
342 assert_ne!(hash1, hash3);
344
345 assert_eq!(hash1.len(), 16);
347 }
348
349 #[test]
350 fn test_create_shared_history() {
351 let history = create_shared_history();
352
353 {
355 let mut write_guard = history.write().unwrap();
356 write_guard.record_read(FileReadRecord::new(
357 PathBuf::from("/tmp/test.txt"),
358 "abc".to_string(),
359 100,
360 ));
361 }
362
363 {
365 let read_guard = history.read().unwrap();
366 assert!(read_guard.has_read(&PathBuf::from("/tmp/test.txt")));
367 }
368 }
369}