synaptic_deep/backend/
state.rs1use async_trait::async_trait;
2use regex::Regex;
3use std::collections::HashMap;
4use std::sync::Arc;
5use synaptic_core::SynapticError;
6use tokio::sync::RwLock;
7
8use super::{Backend, DirEntry, GrepMatch, GrepOutputMode};
9
10pub struct StateBackend {
15 files: Arc<RwLock<HashMap<String, String>>>,
16}
17
18impl StateBackend {
19 pub fn new() -> Self {
20 Self {
21 files: Arc::new(RwLock::new(HashMap::new())),
22 }
23 }
24}
25
26impl Default for StateBackend {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32fn normalize_path(path: &str) -> String {
33 let trimmed = path.trim_matches('/');
34 if trimmed == "." {
35 String::new()
36 } else {
37 trimmed.to_string()
38 }
39}
40
41fn glob_to_regex(pattern: &str) -> String {
42 let mut regex = String::from("^");
43 let mut chars = pattern.chars().peekable();
44
45 while let Some(c) = chars.next() {
46 match c {
47 '*' => {
48 if chars.peek() == Some(&'*') {
49 chars.next();
50 if chars.peek() == Some(&'/') {
51 chars.next();
52 regex.push_str("(.*/)?");
53 } else {
54 regex.push_str(".*");
55 }
56 } else {
57 regex.push_str("[^/]*");
58 }
59 }
60 '?' => regex.push_str("[^/]"),
61 '.' => regex.push_str("\\."),
62 '{' => regex.push('('),
63 '}' => regex.push(')'),
64 ',' => regex.push('|'),
65 '[' => regex.push('['),
66 ']' => regex.push(']'),
67 c => regex.push(c),
68 }
69 }
70 regex.push('$');
71 regex
72}
73
74#[async_trait]
75impl Backend for StateBackend {
76 async fn ls(&self, path: &str) -> Result<Vec<DirEntry>, SynapticError> {
77 let files = self.files.read().await;
78 let prefix = normalize_path(path);
79 let prefix_with_slash = if prefix.is_empty() {
80 String::new()
81 } else {
82 format!("{}/", prefix)
83 };
84
85 let mut entries: HashMap<String, bool> = HashMap::new();
86
87 for key in files.keys() {
88 let rel = if prefix_with_slash.is_empty() {
89 key.clone()
90 } else if let Some(rel) = key.strip_prefix(&prefix_with_slash) {
91 rel.to_string()
92 } else {
93 continue;
94 };
95
96 if let Some(slash_pos) = rel.find('/') {
97 entries.insert(rel[..slash_pos].to_string(), true);
98 } else if !rel.is_empty() {
99 entries.entry(rel).or_insert(false);
100 }
101 }
102
103 let mut result: Vec<DirEntry> = entries
104 .into_iter()
105 .map(|(name, is_dir)| DirEntry {
106 name,
107 is_dir,
108 size: None,
109 })
110 .collect();
111 result.sort_by(|a, b| a.name.cmp(&b.name));
112 Ok(result)
113 }
114
115 async fn read_file(
116 &self,
117 path: &str,
118 offset: usize,
119 limit: usize,
120 ) -> Result<String, SynapticError> {
121 let files = self.files.read().await;
122 let normalized = normalize_path(path);
123 let content = files
124 .get(&normalized)
125 .ok_or_else(|| SynapticError::Tool(format!("file not found: {}", path)))?;
126
127 let lines: Vec<&str> = content.lines().collect();
128 let total = lines.len();
129
130 if offset >= total {
131 return Ok(String::new());
132 }
133
134 let end = (offset + limit).min(total);
135 Ok(lines[offset..end].join("\n"))
136 }
137
138 async fn write_file(&self, path: &str, content: &str) -> Result<(), SynapticError> {
139 let mut files = self.files.write().await;
140 files.insert(normalize_path(path), content.to_string());
141 Ok(())
142 }
143
144 async fn edit_file(
145 &self,
146 path: &str,
147 old_text: &str,
148 new_text: &str,
149 replace_all: bool,
150 ) -> Result<(), SynapticError> {
151 let mut files = self.files.write().await;
152 let normalized = normalize_path(path);
153 let content = files
154 .get(&normalized)
155 .ok_or_else(|| SynapticError::Tool(format!("file not found: {}", path)))?
156 .clone();
157
158 if !content.contains(old_text) {
159 return Err(SynapticError::Tool(format!(
160 "old_string not found in {}",
161 path
162 )));
163 }
164
165 let new_content = if replace_all {
166 content.replace(old_text, new_text)
167 } else {
168 content.replacen(old_text, new_text, 1)
169 };
170
171 files.insert(normalized, new_content);
172 Ok(())
173 }
174
175 async fn glob(&self, pattern: &str, base: &str) -> Result<Vec<String>, SynapticError> {
176 let files = self.files.read().await;
177 let base_normalized = normalize_path(base);
178
179 let regex_str = glob_to_regex(pattern);
180 let re = Regex::new(®ex_str)
181 .map_err(|e| SynapticError::Tool(format!("invalid glob pattern: {}", e)))?;
182
183 let mut matches = Vec::new();
184 for key in files.keys() {
185 let rel = if base_normalized.is_empty() {
186 key.clone()
187 } else if let Some(rel) = key.strip_prefix(&format!("{}/", base_normalized)) {
188 rel.to_string()
189 } else {
190 continue;
191 };
192
193 if re.is_match(&rel) {
194 matches.push(key.clone());
195 }
196 }
197 matches.sort();
198 Ok(matches)
199 }
200
201 async fn grep(
202 &self,
203 pattern: &str,
204 path: Option<&str>,
205 file_glob: Option<&str>,
206 output_mode: GrepOutputMode,
207 ) -> Result<String, SynapticError> {
208 let files = self.files.read().await;
209 let re = Regex::new(pattern)
210 .map_err(|e| SynapticError::Tool(format!("invalid regex: {}", e)))?;
211
212 let glob_re = file_glob.and_then(|g| Regex::new(&glob_to_regex(g)).ok());
213 let base = path.map(normalize_path).unwrap_or_default();
214
215 let mut file_matches: Vec<GrepMatch> = Vec::new();
216 let mut match_files: Vec<String> = Vec::new();
217 let mut match_counts: HashMap<String, usize> = HashMap::new();
218
219 for (file_path, content) in files.iter() {
220 if !base.is_empty() && !file_path.starts_with(&base) {
221 continue;
222 }
223
224 if let Some(ref gre) = glob_re {
225 let rel = if base.is_empty() {
226 file_path.clone()
227 } else {
228 file_path
229 .strip_prefix(&format!("{}/", base))
230 .unwrap_or(file_path)
231 .to_string()
232 };
233 if !gre.is_match(&rel) {
234 continue;
235 }
236 }
237
238 let mut found = false;
239 for (line_num, line) in content.lines().enumerate() {
240 if re.is_match(line) {
241 found = true;
242 file_matches.push(GrepMatch {
243 file: file_path.clone(),
244 line_number: line_num + 1,
245 line: line.to_string(),
246 });
247 *match_counts.entry(file_path.clone()).or_insert(0) += 1;
248 }
249 }
250 if found {
251 match_files.push(file_path.clone());
252 }
253 }
254
255 match output_mode {
256 GrepOutputMode::FilesWithMatches => {
257 match_files.sort();
258 Ok(match_files.join("\n"))
259 }
260 GrepOutputMode::Content => {
261 file_matches
262 .sort_by(|a, b| a.file.cmp(&b.file).then(a.line_number.cmp(&b.line_number)));
263 Ok(file_matches
264 .iter()
265 .map(|m| format!("{}:{}:{}", m.file, m.line_number, m.line))
266 .collect::<Vec<_>>()
267 .join("\n"))
268 }
269 GrepOutputMode::Count => {
270 let mut counts: Vec<_> = match_counts.into_iter().collect();
271 counts.sort_by(|a, b| a.0.cmp(&b.0));
272 Ok(counts
273 .iter()
274 .map(|(f, c)| format!("{}:{}", f, c))
275 .collect::<Vec<_>>()
276 .join("\n"))
277 }
278 }
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_glob_to_regex() {
288 assert_eq!(glob_to_regex("*.rs"), "^[^/]*\\.rs$");
289 assert_eq!(glob_to_regex("**/*.rs"), "^(.*/)?[^/]*\\.rs$");
290 }
291}