Skip to main content

forge_core/edit/
mod.rs

1//! Edit module - Span-safe code editing
2//!
3//! This module provides span-safe refactoring operations via Splice integration.
4
5use crate::error::{ForgeError, Result};
6use crate::types::Span;
7use std::path::PathBuf;
8
9/// Result of an edit operation.
10#[derive(Debug, Clone)]
11pub struct EditResult {
12    /// Whether the operation succeeded
13    pub success: bool,
14    /// Files that were changed
15    pub changed_files: Vec<PathBuf>,
16    /// Optional error message
17    pub error: Option<String>,
18}
19
20impl EditResult {
21    /// Creates a successful result.
22    pub fn success(files: Vec<PathBuf>) -> Self {
23        Self {
24            success: true,
25            changed_files: files,
26            error: None,
27        }
28    }
29    
30    /// Creates a failed result.
31    pub fn failure(error: String) -> Self {
32        Self {
33            success: false,
34            changed_files: Vec::new(),
35            error: Some(error),
36        }
37    }
38}
39
40/// Edit module for span-safe refactoring.
41pub struct EditModule {
42    store: std::sync::Arc<crate::storage::UnifiedGraphStore>,
43}
44
45impl EditModule {
46    /// Create a new EditModule.
47    pub fn new(store: std::sync::Arc<crate::storage::UnifiedGraphStore>) -> Self {
48        Self { store }
49    }
50
51    /// Apply an edit operation.
52    pub async fn apply(&mut self, _op: EditOperation) -> Result<()> {
53        Ok(())
54    }
55    
56    /// Patches a symbol with new content.
57    ///
58    /// Finds the symbol definition and replaces it with the new content.
59    ///
60    /// # Arguments
61    ///
62    /// * `symbol` - Symbol name to patch
63    /// * `replacement` - New content for the symbol
64    ///
65    /// # Returns
66    ///
67    /// Result indicating success/failure and changed files.
68    pub async fn patch_symbol(
69        &self,
70        symbol: &str,
71        replacement: &str
72    ) -> Result<EditResult> {
73        
74        
75        // Find the symbol in the codebase
76        let codebase_path = &self.store.codebase_path;
77        
78        // Search for files containing the symbol definition
79        let mut changed_files = Vec::new();
80        
81        // Scan Rust files for the symbol definition
82        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                // Read file and look for symbol definition
111                let content = fs::read_to_string(&path).await
112                    .map_err(|e| ForgeError::DatabaseError(format!("Failed to read file: {}", e)))?;
113                
114                // Look for function definition pattern
115                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                        // Find the end of the function (matching braces)
128                        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                // Also check for struct/impl definitions
137                if !found {
138                    let struct_pattern = format!("struct {} ", symbol);
139                    if let Some(start_idx) = modified.find(&struct_pattern) {
140                        // Find end of struct definition
141                        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    /// Renames a symbol and updates all references.
161    ///
162    /// # Arguments
163    ///
164    /// * `old_name` - Current symbol name
165    /// * `new_name` - New symbol name
166    ///
167    /// # Returns
168    ///
169    /// Result indicating success/failure.
170    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        // Scan all Rust files and replace occurrences
181        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                // Simple word-boundary replacement
213                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
228/// Find the end of a function definition, handling nested braces
229fn find_function_end(content: &str, start_idx: usize) -> Option<usize> {
230    let after_sig = &content[start_idx..];
231    
232    // Find opening brace
233    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
263/// Find the end of a struct definition
264fn find_struct_end(content: &str, start_idx: usize) -> Option<usize> {
265    let after_keyword = &content[start_idx..];
266    
267    // Find opening brace or semicolon
268    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
291/// Replace occurrences with word boundaries
292fn 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        // Check word boundaries
298        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
316/// An edit operation.
317pub enum EditOperation {
318    /// Replace a span with new content.
319    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        // EditModule::new requires UnifiedGraphStore
332        // For now just test that the type exists
333        use crate::storage::UnifiedGraphStore;
334        // Can't easily create without async, but verify type exists
335        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}