matrixcode_core/tools/
read_history.rs1use chrono::{DateTime, Utc};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone, Default)]
14pub struct ReadHistoryTracker {
15 read_files: HashSet<String>,
17
18 read_timestamps: HashMap<String, DateTime<Utc>>,
20}
21
22impl ReadHistoryTracker {
23 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn mark_read(&mut self, file_path: &str) {
38 let normalized = self.normalize_path(file_path);
39 self.read_files.insert(normalized.clone());
40 self.read_timestamps.insert(normalized, Utc::now());
41 }
42
43 pub fn has_read(&self, file_path: &str) -> bool {
51 let normalized = self.normalize_path(file_path);
52 self.read_files.contains(&normalized)
53 }
54
55 pub fn clear(&mut self) {
57 self.read_files.clear();
58 self.read_timestamps.clear();
59 }
60
61 pub fn count(&self) -> usize {
63 self.read_files.len()
64 }
65
66 pub fn read_files(&self) -> Vec<&String> {
68 self.read_files.iter().collect()
69 }
70
71 pub fn read_timestamp(&self, file_path: &str) -> Option<DateTime<Utc>> {
73 let normalized = self.normalize_path(file_path);
74 self.read_timestamps.get(&normalized).copied()
75 }
76
77 fn normalize_path(&self, path: &str) -> String {
83 let normalized = path.replace('\\', "/");
85 if normalized.len() > 1 && normalized.ends_with('/') {
87 normalized.trim_end_matches('/').to_string()
88 } else {
89 normalized
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
99pub struct MustReadFirstError {
100 pub file: String,
102 pub message: String,
104}
105
106impl MustReadFirstError {
107 pub fn new(file: impl Into<String>) -> Self {
109 let file_path = file.into();
110 Self {
111 file: file_path.clone(),
112 message: format!(
113 "File '{}' has not been read in this session. \
114 Please use the 'read' tool to read the file first before editing or writing. \
115 This ensures you understand the current file content and context.",
116 file_path
117 ),
118 }
119 }
120
121 pub fn file(&self) -> &str {
123 &self.file
124 }
125
126 pub fn message(&self) -> &str {
128 &self.message
129 }
130}
131
132impl std::fmt::Display for MustReadFirstError {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 write!(f, "{}", self.message)
135 }
136}
137
138impl std::error::Error for MustReadFirstError {}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_mark_and_check_read() {
146 let mut tracker = ReadHistoryTracker::new();
147
148 assert!(!tracker.has_read("test.txt"));
150
151 tracker.mark_read("test.txt");
153
154 assert!(tracker.has_read("test.txt"));
156 }
157
158 #[test]
159 fn test_path_normalization() {
160 let mut tracker = ReadHistoryTracker::new();
161
162 tracker.mark_read("C:\\Users\\test\\file.txt");
164
165 assert!(tracker.has_read("C:/Users/test/file.txt"));
167
168 tracker.mark_read("/home/user/file.txt");
170 assert!(tracker.has_read("/home/user/file.txt"));
171 }
172
173 #[test]
174 fn test_trailing_slash() {
175 let mut tracker = ReadHistoryTracker::new();
176
177 tracker.mark_read("path/to/file");
178
179 assert!(tracker.has_read("path/to/file/"));
181 }
182
183 #[test]
184 fn test_clear() {
185 let mut tracker = ReadHistoryTracker::new();
186
187 tracker.mark_read("file1.txt");
188 tracker.mark_read("file2.txt");
189
190 assert_eq!(tracker.count(), 2);
191
192 tracker.clear();
193
194 assert_eq!(tracker.count(), 0);
195 assert!(!tracker.has_read("file1.txt"));
196 }
197
198 #[test]
199 fn test_error_message() {
200 let error = MustReadFirstError::new("test.rs");
201
202 assert_eq!(error.file(), "test.rs");
203 assert!(error.message().contains("test.rs"));
204 assert!(error.message().contains("read"));
205 }
206}