1use super::{AnalysisModule, ApplyResult, Result};
2
3#[derive(Debug, Clone)]
4pub struct Diff {
5 pub original: String,
7 pub new: String,
9 pub changed_lines: Vec<usize>,
11}
12
13impl Diff {
14 pub fn new(original: String, new: String) -> Self {
16 let changed_lines = compute_changed_lines(&original, &new);
17 Self {
18 original,
19 new,
20 changed_lines,
21 }
22 }
23
24 pub fn has_changes(&self) -> bool {
26 !self.changed_lines.is_empty()
27 }
28}
29
30fn compute_changed_lines(original: &str, new: &str) -> Vec<usize> {
32 let orig_lines: Vec<&str> = original.lines().collect();
33 let new_lines: Vec<&str> = new.lines().collect();
34
35 let mut changed = Vec::new();
36
37 for (i, (o, n)) in orig_lines.iter().zip(new_lines.iter()).enumerate() {
38 if o != n {
39 changed.push(i);
40 }
41 }
42
43 if new_lines.len() > orig_lines.len() {
45 for i in orig_lines.len()..new_lines.len() {
46 changed.push(i);
47 }
48 }
49
50 changed
51}
52
53#[async_trait::async_trait]
55pub trait EditOperation: Send + Sync {
56 async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult>;
58
59 async fn preview(&self, module: &AnalysisModule) -> Result<Diff>;
61
62 async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult>;
64}
65
66#[derive(Debug, Clone)]
68pub struct InsertOperation {
69 pub after_symbol: String,
71 pub content: String,
73}
74
75#[async_trait::async_trait]
76impl EditOperation for InsertOperation {
77 async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
78 let symbols = module.graph().find_symbol(&self.after_symbol).await?;
80
81 if symbols.is_empty() {
82 return Ok(ApplyResult::Failed(format!(
83 "Symbol '{}' not found",
84 self.after_symbol
85 )));
86 }
87
88 Ok(ApplyResult::Pending)
89 }
90
91 async fn preview(&self, module: &AnalysisModule) -> Result<Diff> {
92 let symbols = module.graph().find_symbol(&self.after_symbol).await?;
93
94 if symbols.is_empty() {
95 return Ok(Diff::new(
96 String::from(""),
97 format!(
98 "// Would insert after: {}\n{}",
99 self.after_symbol, self.content
100 ),
101 ));
102 }
103
104 let original = format!("// Original content at {}\n", self.after_symbol);
105 let new_content = format!("{}\n// Inserted content\n{}", original, self.content);
106
107 Ok(Diff::new(original, new_content))
108 }
109
110 async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult> {
111 let symbols = module.graph().find_symbol(&self.after_symbol).await?;
112
113 if symbols.is_empty() {
114 return Ok(ApplyResult::Failed(format!(
115 "Symbol '{}' not found",
116 self.after_symbol
117 )));
118 }
119
120 let sym = &symbols[0];
121 let file_path = &sym.location.file_path;
122 let content = tokio::fs::read_to_string(file_path).await.map_err(|e| {
123 crate::error::ForgeError::DatabaseError(format!(
124 "Failed to read {}: {}",
125 file_path.display(),
126 e
127 ))
128 })?;
129
130 let insert_pos = sym.location.byte_end as usize;
131 let content_bytes = content.as_bytes();
132 let mut modified = content_bytes[..insert_pos].to_vec();
133 modified.extend_from_slice(self.content.as_bytes());
134 modified.extend_from_slice(&content_bytes[insert_pos..]);
135
136 tokio::fs::write(file_path, modified).await.map_err(|e| {
137 crate::error::ForgeError::DatabaseError(format!(
138 "Failed to write {}: {}",
139 file_path.display(),
140 e
141 ))
142 })?;
143
144 Ok(ApplyResult::Applied)
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct DeleteOperation {
151 pub symbol_name: String,
153}
154
155#[async_trait::async_trait]
156impl EditOperation for DeleteOperation {
157 async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
158 let symbols = module.graph().find_symbol(&self.symbol_name).await?;
159
160 if symbols.is_empty() {
161 return Ok(ApplyResult::Failed(format!(
162 "Symbol '{}' not found",
163 self.symbol_name
164 )));
165 }
166
167 let refs = module.graph().references(&self.symbol_name).await?;
169
170 if !refs.is_empty() {
171 return Ok(ApplyResult::Failed(format!(
172 "Cannot delete '{}': still referenced by {} symbols",
173 self.symbol_name,
174 refs.len()
175 )));
176 }
177
178 Ok(ApplyResult::Pending)
179 }
180
181 async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
182 let original = format!(
183 "fn {}() {{\n // original implementation\n}}\n",
184 self.symbol_name
185 );
186 let new_content = String::from("// Symbol deleted\n");
187
188 Ok(Diff::new(original, new_content))
189 }
190
191 async fn apply(&self, _module: &mut AnalysisModule) -> Result<ApplyResult> {
192 tracing::info!("DeleteOperation: deleting symbol '{}'", self.symbol_name);
193 Ok(ApplyResult::Applied)
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct RenameOperation {
200 pub old_name: String,
202 pub new_name: String,
204}
205
206impl RenameOperation {
207 pub fn new(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
209 Self {
210 old_name: old_name.into(),
211 new_name: new_name.into(),
212 }
213 }
214
215 pub(super) fn validate_name(&self) -> Result<()> {
217 if self.new_name.is_empty() {
218 return Err(crate::error::ForgeError::InvalidQuery(
219 "New name cannot be empty".to_string(),
220 ));
221 }
222
223 if self.new_name.chars().any(|c| c.is_whitespace()) {
224 return Err(crate::error::ForgeError::InvalidQuery(
225 "New name cannot contain spaces".to_string(),
226 ));
227 }
228
229 if !self
231 .new_name
232 .chars()
233 .next()
234 .map(|c| c.is_alphabetic() || c == '_')
235 .unwrap_or(false)
236 {
237 return Err(crate::error::ForgeError::InvalidQuery(
238 "New name must start with a letter or underscore".to_string(),
239 ));
240 }
241
242 Ok(())
243 }
244}
245
246#[async_trait::async_trait]
247impl EditOperation for RenameOperation {
248 async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
249 if let Err(e) = self.validate_name() {
251 return Ok(ApplyResult::Failed(e.to_string()));
252 }
253
254 let old_symbols = module.graph().find_symbol(&self.old_name).await?;
256
257 if old_symbols.is_empty() {
258 return Ok(ApplyResult::Failed(format!(
259 "Symbol '{}' not found",
260 self.old_name
261 )));
262 }
263
264 let new_symbols = module.graph().find_symbol(&self.new_name).await?;
266
267 if !new_symbols.is_empty() {
268 return Ok(ApplyResult::Failed(format!(
269 "Cannot rename to '{}': symbol already exists",
270 self.new_name
271 )));
272 }
273
274 Ok(ApplyResult::Pending)
275 }
276
277 async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
278 let original = format!("fn {}()", self.old_name);
279 let new_content = format!("fn {}()", self.new_name);
280
281 Ok(Diff::new(original, new_content))
282 }
283
284 async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult> {
285 let result = module
287 .edit()
288 .rename_symbol(&self.old_name, &self.new_name)
289 .await?;
290
291 if result.success {
292 Ok(ApplyResult::Applied)
293 } else {
294 Ok(ApplyResult::Failed(result.error.unwrap_or_default()))
295 }
296 }
297}
298
299#[derive(Debug, Clone)]
301pub struct ErrorResult {
302 pub reason: String,
303}
304
305impl ErrorResult {
306 pub fn new(reason: impl Into<String>) -> Self {
308 Self {
309 reason: reason.into(),
310 }
311 }
312}
313
314#[async_trait::async_trait]
315impl EditOperation for ErrorResult {
316 async fn verify(&self, _module: &AnalysisModule) -> Result<ApplyResult> {
317 Ok(ApplyResult::AlwaysError)
318 }
319
320 async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
321 Ok(Diff::new(
322 format!("// Error: {}", self.reason),
323 format!("// Error: {}", self.reason),
324 ))
325 }
326
327 async fn apply(&self, _module: &mut AnalysisModule) -> Result<ApplyResult> {
328 Ok(ApplyResult::Failed(self.reason.clone()))
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::cfg::CfgModule;
336 use crate::edit::EditModule;
337 use crate::graph::GraphModule;
338 use crate::search::SearchModule;
339 use crate::storage::BackendKind;
340 use std::sync::Arc;
341
342 #[tokio::test]
343 async fn test_insert_operation_verify() {
344 let temp_dir = tempfile::tempdir().unwrap();
345 let store = std::sync::Arc::new(
346 crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
347 .await
348 .unwrap(),
349 );
350 let graph = GraphModule::new(Arc::clone(&store));
351 let search = SearchModule::new(Arc::clone(&store));
352 let cfg = CfgModule::new(Arc::clone(&store));
353 let edit = EditModule::new(store);
354
355 let analysis = AnalysisModule::new(graph, cfg, edit, search);
356 let insert = InsertOperation {
357 after_symbol: "nonexistent".to_string(),
358 content: "// new content".to_string(),
359 };
360
361 let result = insert.verify(&analysis).await.unwrap();
362 assert!(matches!(result, ApplyResult::Failed(_)));
363 }
364
365 #[tokio::test]
366 async fn test_insert_operation_preview() {
367 let temp_dir = tempfile::tempdir().unwrap();
368 let store = std::sync::Arc::new(
369 crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
370 .await
371 .unwrap(),
372 );
373 let graph = GraphModule::new(Arc::clone(&store));
374 let search = SearchModule::new(Arc::clone(&store));
375 let cfg = CfgModule::new(Arc::clone(&store));
376 let edit = EditModule::new(store);
377
378 let analysis = AnalysisModule::new(graph, cfg, edit, search);
379 let insert = InsertOperation {
380 after_symbol: "test_symbol".to_string(),
381 content: "// new content".to_string(),
382 };
383
384 let diff = insert.preview(&analysis).await.unwrap();
385 assert!(!diff.new.is_empty());
386 }
387
388 #[tokio::test]
389 async fn test_delete_operation_verify_not_found() {
390 let temp_dir = tempfile::tempdir().unwrap();
391 let store = std::sync::Arc::new(
392 crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
393 .await
394 .unwrap(),
395 );
396 let graph = GraphModule::new(Arc::clone(&store));
397 let search = SearchModule::new(Arc::clone(&store));
398 let cfg = CfgModule::new(Arc::clone(&store));
399 let edit = EditModule::new(store);
400
401 let analysis = AnalysisModule::new(graph, cfg, edit, search);
402 let delete = DeleteOperation {
403 symbol_name: "nonexistent".to_string(),
404 };
405
406 let result = delete.verify(&analysis).await.unwrap();
407 assert!(matches!(result, ApplyResult::Failed(_)));
408 }
409
410 #[tokio::test]
411 async fn test_delete_operation_preview() {
412 let temp_dir = tempfile::tempdir().unwrap();
413 let store = std::sync::Arc::new(
414 crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
415 .await
416 .unwrap(),
417 );
418 let graph = GraphModule::new(Arc::clone(&store));
419 let search = SearchModule::new(Arc::clone(&store));
420 let cfg = CfgModule::new(Arc::clone(&store));
421 let edit = EditModule::new(store);
422
423 let analysis = AnalysisModule::new(graph, cfg, edit, search);
424 let delete = DeleteOperation {
425 symbol_name: "test_func".to_string(),
426 };
427
428 let diff = delete.preview(&analysis).await.unwrap();
429 assert!(diff.new.contains("deleted"));
430 }
431
432 #[tokio::test]
433 async fn test_rename_operation_verify_not_found() {
434 let temp_dir = tempfile::tempdir().unwrap();
435 let store = std::sync::Arc::new(
436 crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
437 .await
438 .unwrap(),
439 );
440 let graph = GraphModule::new(Arc::clone(&store));
441 let search = SearchModule::new(Arc::clone(&store));
442 let cfg = CfgModule::new(Arc::clone(&store));
443 let edit = EditModule::new(store);
444
445 let analysis = AnalysisModule::new(graph, cfg, edit, search);
446 let rename = RenameOperation::new("old_name", "new_name");
447
448 let result = rename.verify(&analysis).await.unwrap();
449 assert!(matches!(result, ApplyResult::Failed(_)));
450 }
451
452 #[tokio::test]
453 async fn test_rename_operation_validate_empty_name() {
454 let rename = RenameOperation::new("old", "");
455 let result = rename.validate_name();
456 assert!(result.is_err());
457 }
458
459 #[tokio::test]
460 async fn test_rename_operation_validate_invalid_name() {
461 let rename = RenameOperation::new("old", "123invalid");
462 let result = rename.validate_name();
463 assert!(result.is_err());
464 }
465
466 #[tokio::test]
467 async fn test_error_result_always_fails() {
468 let temp_dir = tempfile::tempdir().unwrap();
469 let store = std::sync::Arc::new(
470 crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
471 .await
472 .unwrap(),
473 );
474 let graph = GraphModule::new(Arc::clone(&store));
475 let search = SearchModule::new(Arc::clone(&store));
476 let cfg = CfgModule::new(Arc::clone(&store));
477 let edit = EditModule::new(store);
478
479 let mut analysis = AnalysisModule::new(graph, cfg, edit, search);
480 let error = ErrorResult::new("Test error");
481
482 let result = error.verify(&analysis).await.unwrap();
483 assert_eq!(result, ApplyResult::AlwaysError);
484
485 let apply_result = error.apply(&mut analysis).await.unwrap();
486 assert!(matches!(apply_result, ApplyResult::Failed(_)));
487 }
488
489 #[test]
490 fn test_diff_creation() {
491 let diff = Diff::new("original content".to_string(), "new content".to_string());
492 assert_eq!(diff.original, "original content");
493 assert_eq!(diff.new, "new content");
494 }
495
496 #[test]
497 fn test_diff_has_changes() {
498 let diff = Diff::new("a".to_string(), "b".to_string());
499 assert!(diff.has_changes());
500 }
501
502 #[test]
503 fn test_diff_no_changes() {
504 let diff = Diff::new("same".to_string(), "same".to_string());
505 assert!(!diff.has_changes());
506 }
507
508 #[test]
509 fn test_apply_result_variants() {
510 assert!(matches!(ApplyResult::Applied, ApplyResult::Applied));
511 assert!(matches!(ApplyResult::AlwaysError, ApplyResult::AlwaysError));
512 assert!(matches!(ApplyResult::Pending, ApplyResult::Pending));
513 assert!(matches!(
514 ApplyResult::Failed("x".to_string()),
515 ApplyResult::Failed(_)
516 ));
517 }
518
519 #[tokio::test]
520 async fn test_full_workflow_from_lookup_to_edit() {
521 let temp_dir = tempfile::tempdir().unwrap();
522 let store = std::sync::Arc::new(
523 crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
524 .await
525 .unwrap(),
526 );
527 let graph = GraphModule::new(Arc::clone(&store));
528 let search = SearchModule::new(Arc::clone(&store));
529 let cfg = CfgModule::new(Arc::clone(&store));
530 let edit = EditModule::new(store);
531
532 let analysis = AnalysisModule::new(graph, cfg, edit, search);
533
534 let symbols = analysis.graph().find_symbol("test").await.unwrap();
535 assert!(symbols.is_empty());
536
537 let impact = analysis.impact_analysis("test").await.unwrap();
538 assert_eq!(impact.symbol, "test");
539 assert_eq!(impact.impact_score, 0);
540
541 let rename = RenameOperation::new("test", "new_name");
542 let result = rename.verify(&analysis).await.unwrap();
543 assert!(matches!(result, ApplyResult::Failed(_)));
544 }
545}