1use crate::error::{ForgeError, Result};
6use crate::types::Span;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone)]
11pub struct EditResult {
12 pub success: bool,
14 pub changed_files: Vec<PathBuf>,
16 pub error: Option<String>,
18}
19
20impl EditResult {
21 pub fn success(files: Vec<PathBuf>) -> Self {
23 Self {
24 success: true,
25 changed_files: files,
26 error: None,
27 }
28 }
29
30 pub fn failure(error: String) -> Self {
32 Self {
33 success: false,
34 changed_files: Vec::new(),
35 error: Some(error),
36 }
37 }
38}
39
40pub struct EditModule {
42 store: std::sync::Arc<crate::storage::UnifiedGraphStore>,
43}
44
45impl EditModule {
46 pub fn new(store: std::sync::Arc<crate::storage::UnifiedGraphStore>) -> Self {
48 Self { store }
49 }
50
51 pub async fn apply(&mut self, _op: EditOperation) -> Result<()> {
53 Ok(())
54 }
55
56 pub async fn patch_symbol(
69 &self,
70 symbol: &str,
71 replacement: &str
72 ) -> Result<EditResult> {
73
74
75 let codebase_path = &self.store.codebase_path;
77
78 let mut changed_files = Vec::new();
80
81 Self::patch_symbol_in_dir(codebase_path, codebase_path, symbol, replacement, &mut changed_files).await?;
83
84 if changed_files.is_empty() {
85 return Err(ForgeError::SymbolNotFound(format!("Symbol '{}' not found", symbol)));
86 }
87
88 Ok(EditResult::success(changed_files))
89 }
90
91 async fn patch_symbol_in_dir(
92 root: &std::path::Path,
93 dir: &std::path::Path,
94 symbol: &str,
95 replacement: &str,
96 changed_files: &mut Vec<PathBuf>,
97 ) -> Result<()> {
98 use tokio::fs;
99
100 let mut entries = fs::read_dir(dir).await
101 .map_err(|e| ForgeError::DatabaseError(format!("Failed to read dir: {}", e)))?;
102
103 while let Some(entry) = entries.next_entry().await
104 .map_err(|e| ForgeError::DatabaseError(format!("Failed to read entry: {}", e)))?
105 {
106 let path = entry.path();
107 if path.is_dir() {
108 Box::pin(Self::patch_symbol_in_dir(root, &path, symbol, replacement, changed_files)).await?;
109 } else if path.is_file() && path.extension().map(|e| e == "rs").unwrap_or(false) {
110 let content = fs::read_to_string(&path).await
112 .map_err(|e| ForgeError::DatabaseError(format!("Failed to read file: {}", e)))?;
113
114 let patterns = vec![
116 format!("fn {}(", symbol),
117 format!("pub fn {}(", symbol),
118 format!("async fn {}(", symbol),
119 format!("pub async fn {}(", symbol),
120 ];
121
122 let mut modified = content.clone();
123 let mut found = false;
124
125 for pattern in &patterns {
126 if let Some(start_idx) = modified.find(pattern) {
127 if let Some(end_idx) = find_function_end(&modified, start_idx) {
129 modified.replace_range(start_idx..end_idx, replacement);
130 found = true;
131 break;
132 }
133 }
134 }
135
136 if !found {
138 let struct_pattern = format!("struct {} ", symbol);
139 if let Some(start_idx) = modified.find(&struct_pattern) {
140 if let Some(end_idx) = find_struct_end(&modified, start_idx) {
142 modified.replace_range(start_idx..end_idx, replacement);
143 found = true;
144 }
145 }
146 }
147
148 if found {
149 fs::write(&path, modified).await
150 .map_err(|e| ForgeError::DatabaseError(format!("Failed to write file: {}", e)))?;
151 let relative_path = path.strip_prefix(root).unwrap_or(&path);
152 changed_files.push(relative_path.to_path_buf());
153 }
154 }
155 }
156
157 Ok(())
158 }
159
160 pub async fn rename_symbol(
171 &self,
172 old_name: &str,
173 new_name: &str
174 ) -> Result<EditResult> {
175
176
177 let codebase_path = &self.store.codebase_path;
178 let mut changed_files = Vec::new();
179
180 Self::rename_in_dir(codebase_path, codebase_path, old_name, new_name, &mut changed_files).await?;
182
183 if changed_files.is_empty() {
184 return Err(ForgeError::SymbolNotFound(format!("Symbol '{}' not found", old_name)));
185 }
186
187 Ok(EditResult::success(changed_files))
188 }
189
190 async fn rename_in_dir(
191 root: &std::path::Path,
192 dir: &std::path::Path,
193 old_name: &str,
194 new_name: &str,
195 changed_files: &mut Vec<PathBuf>,
196 ) -> Result<()> {
197 use tokio::fs;
198
199 let mut entries = fs::read_dir(dir).await
200 .map_err(|e| ForgeError::DatabaseError(format!("Failed to read dir: {}", e)))?;
201
202 while let Some(entry) = entries.next_entry().await
203 .map_err(|e| ForgeError::DatabaseError(format!("Failed to read entry: {}", e)))?
204 {
205 let path = entry.path();
206 if path.is_dir() {
207 Box::pin(Self::rename_in_dir(root, &path, old_name, new_name, changed_files)).await?;
208 } else if path.is_file() && path.extension().map(|e| e == "rs").unwrap_or(false) {
209 let content = fs::read_to_string(&path).await
210 .map_err(|e| ForgeError::DatabaseError(format!("Failed to read file: {}", e)))?;
211
212 let modified = replace_word_boundaries(&content, old_name, new_name);
214
215 if modified != content {
216 fs::write(&path, modified).await
217 .map_err(|e| ForgeError::DatabaseError(format!("Failed to write file: {}", e)))?;
218 let relative_path = path.strip_prefix(root).unwrap_or(&path);
219 changed_files.push(relative_path.to_path_buf());
220 }
221 }
222 }
223
224 Ok(())
225 }
226}
227
228fn find_function_end(content: &str, start_idx: usize) -> Option<usize> {
230 let after_sig = &content[start_idx..];
231
232 if let Some(brace_idx) = after_sig.find('{') {
234 let body_start = start_idx + brace_idx + 1;
235 let mut brace_count = 1;
236 let mut in_string = false;
237 let mut escape_next = false;
238
239 for (i, c) in content[body_start..].char_indices() {
240 if escape_next {
241 escape_next = false;
242 continue;
243 }
244
245 match c {
246 '\\' if in_string => escape_next = true,
247 '"' | '\'' => in_string = !in_string,
248 '{' if !in_string => brace_count += 1,
249 '}' if !in_string => {
250 brace_count -= 1;
251 if brace_count == 0 {
252 return Some(body_start + i + 1);
253 }
254 }
255 _ => {}
256 }
257 }
258 }
259
260 None
261}
262
263fn find_struct_end(content: &str, start_idx: usize) -> Option<usize> {
265 let after_keyword = &content[start_idx..];
266
267 if let Some(brace_idx) = after_keyword.find('{') {
269 let body_start = start_idx + brace_idx + 1;
270 let mut brace_count = 1;
271
272 for (i, c) in content[body_start..].char_indices() {
273 match c {
274 '{' => brace_count += 1,
275 '}' => {
276 brace_count -= 1;
277 if brace_count == 0 {
278 return Some(body_start + i + 1);
279 }
280 }
281 _ => {}
282 }
283 }
284 } else if let Some(semi_idx) = after_keyword.find(';') {
285 return Some(start_idx + semi_idx + 1);
286 }
287
288 None
289}
290
291fn replace_word_boundaries(content: &str, old: &str, new: &str) -> String {
293 let mut result = String::new();
294 let mut last_end = 0;
295
296 for (i, _) in content.match_indices(old) {
297 let before = if i > 0 { content.chars().nth(i - 1) } else { None };
299 let after = content.chars().nth(i + old.len());
300
301 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
302 let word_before = before.map(is_word_char).unwrap_or(false);
303 let word_after = after.map(is_word_char).unwrap_or(false);
304
305 if !word_before && !word_after {
306 result.push_str(&content[last_end..i]);
307 result.push_str(new);
308 last_end = i + old.len();
309 }
310 }
311
312 result.push_str(&content[last_end..]);
313 result
314}
315
316pub enum EditOperation {
318 Replace {
320 span: Span,
321 new_content: String,
322 },
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_edit_module_creation() {
331 use crate::storage::UnifiedGraphStore;
334 let _store: Option<UnifiedGraphStore> = None;
336 }
337
338 #[test]
339 fn test_edit_operation_replace() {
340 let span = Span { start: 10, end: 20 };
341 let _op = EditOperation::Replace {
342 span,
343 new_content: String::from("test"),
344 };
345 }
346}