1use anyhow::{Context, Result};
6use proc_macro2::{LineColumn, Span};
7use syn::{
8 parse_str, File, Item, ItemEnum, ItemStruct,
9 Fields, Field, spanned::Spanned, Arm, ExprMatch, ExprStruct,
10 visit_mut::VisitMut, Expr,
11};
12use quote::ToTokens;
13
14use crate::operations::*;
15use crate::path_resolver::PathResolver;
16use prettyplease;
17
18pub struct RustEditor {
19 content: String,
20 syntax_tree: File,
21 line_offsets: Vec<usize>, }
23
24impl RustEditor {
25 pub fn new(content: &str) -> Result<Self> {
26 let syntax_tree: File = syn::parse_str(content)
27 .map_err(|e| anyhow::anyhow!("Failed to parse Rust code at line {}, column {}: {}", e.span().start().line, e.span().start().column, e))?;
28
29 let line_offsets = Self::compute_line_offsets(content);
30
31 Ok(Self {
32 content: content.to_string(),
33 syntax_tree,
34 line_offsets,
35 })
36 }
37
38 fn format_field(field: &Field) -> String {
40 let mut result = String::new();
41
42 if let syn::Visibility::Public(_) = field.vis {
44 result.push_str("pub ");
45 }
46
47 if let Some(ident) = &field.ident {
49 result.push_str(&ident.to_string());
50 }
51
52 result.push_str(": ");
54
55 let type_str = field.ty.to_token_stream().to_string();
57 let type_str = type_str.replace(" < ", "<").replace(" >", ">");
58 result.push_str(&type_str);
59
60 result
61 }
62
63 fn compute_line_offsets(content: &str) -> Vec<usize> {
64 let mut offsets = vec![0];
65 for (i, ch) in content.char_indices() {
66 if ch == '\n' {
67 offsets.push(i + 1);
68 }
69 }
70 offsets
71 }
72
73 fn find_similar_fields(target: &str, available: &[String]) -> Vec<String> {
75 use strsim::levenshtein;
76
77 let mut scored: Vec<_> = available.iter()
78 .map(|field| (field, levenshtein(target, field)))
79 .filter(|(_, distance)| *distance <= 3) .collect();
81
82 scored.sort_by_key(|(_, distance)| *distance);
83 scored.into_iter()
84 .take(3) .map(|(field, _)| field.to_string())
86 .collect()
87 }
88
89 pub fn apply_operation(&mut self, op: &Operation) -> Result<ModificationResult> {
90 match op {
91 Operation::AddStructField(op) => self.add_struct_field(op),
92 Operation::UpdateStructField(op) => self.update_struct_field(op),
93 Operation::RemoveStructField(op) => self.remove_struct_field(op),
94 Operation::AddStructLiteralField(op) => self.add_struct_literal_field(op),
95 Operation::AddEnumVariant(op) => self.add_enum_variant(op),
96 Operation::UpdateEnumVariant(op) => self.update_enum_variant(op),
97 Operation::RemoveEnumVariant(op) => self.remove_enum_variant(op),
98 Operation::AddMatchArm(op) => self.add_match_arm(op),
99 Operation::UpdateMatchArm(op) => self.update_match_arm(op),
100 Operation::RemoveMatchArm(op) => self.remove_match_arm(op),
101 Operation::AddImplMethod(op) => self.add_impl_method(op),
102 Operation::AddUseStatement(op) => self.add_use_statement(op),
103 Operation::AddDerive(op) => self.add_derive(op),
104 Operation::Transform(op) => self.transform(op),
105 Operation::RenameEnumVariant(op) => self.rename_enum_variant(op),
106 Operation::RenameFunction(op) => self.rename_function(op),
107 Operation::AddDocComment(op) => self.add_doc_comment_surgical(
108 &op.target_type,
109 &op.name,
110 &op.doc_comment,
111 &op.style,
112 ),
113 Operation::UpdateDocComment(op) => self.update_doc_comment_surgical(
114 &op.target_type,
115 &op.name,
116 &op.doc_comment,
117 &DocCommentStyle::Line, ),
119 Operation::RemoveDocComment(op) => self.remove_doc_comment_surgical(
120 &op.target_type,
121 &op.name,
122 ),
123 Operation::SetStructLiteralBase(op) => self.set_struct_literal_base(op),
124 Operation::AddCallArg(op) => self.add_call_arg(op),
125 Operation::UpdateCallArg(op) => self.update_call_arg(op),
126 Operation::RemoveCallArg(op) => self.remove_call_arg(op),
127 }
128 }
129
130 pub(crate) fn add_struct_field(&mut self, op: &AddStructFieldOp) -> Result<ModificationResult> {
131 let mut modified_nodes = Vec::new();
132
133 let is_enum_variant = op.struct_name.contains("::");
135
136 let has_type = op.field_def.contains(':');
139 let is_literal_only = op.literal_default.is_some() && !has_type;
140
141 if is_enum_variant || is_literal_only {
145 let final_field_def = if let Some(literal_default) = &op.literal_default {
152 let field_name = op.field_def.split(':')
154 .next()
155 .map(|s| s.trim().to_string())
156 .context("Failed to extract field name")?;
157 format!("{}: {}", field_name, literal_default)
158 } else if op.field_def.contains(':') {
159 op.field_def.clone()
161 } else {
162 anyhow::bail!(
163 "For enum variant literals, field definition must include a value.\n\
164 Either use: --field \"layer: None\" or --field \"layer\" --literal-default \"None\""
165 );
166 };
167
168 let literal_op = AddStructLiteralFieldOp {
170 struct_name: op.struct_name.clone(),
171 field_def: final_field_def,
172 position: op.position.clone(),
173 struct_path: None,
174 };
175
176 let literal_result = self.add_struct_literal_field(&literal_op)
178 .context("Failed to update struct literals")?;
179
180 return Ok(literal_result);
181 }
182
183 let item_struct = self.syntax_tree.items.iter()
185 .find_map(|item| {
186 if let Item::Struct(s) = item {
187 if s.ident == op.struct_name {
188 return Some(s.clone());
189 }
190 }
191 None
192 })
193 .ok_or_else(|| anyhow::anyhow!("Struct '{}' not found", op.struct_name))?;
194
195 if let Some(ref where_filter) = op.where_filter {
197 if !self.matches_where_filter(&item_struct.attrs, where_filter)? {
198 return Ok(ModificationResult {
200 changed: false,
201 modified_nodes: vec![],
202 unmatched_qualified_paths: None,
203 });
204 }
205 }
206
207 if op.literal_default.is_none() {
209 let backup_node = BackupNode {
211 node_type: "struct".to_string(),
212 identifier: op.struct_name.clone(),
213 original_content: self.unparse_item(&Item::Struct(item_struct.clone())),
214 location: self.span_to_location(item_struct.span()),
215 };
216
217 let modified = self.insert_struct_field(&item_struct, op)
219 .context("Failed to add field to struct definition")?;
220
221 if !modified {
222 return Ok(ModificationResult {
223 changed: false,
224 modified_nodes: vec![],
225 unmatched_qualified_paths: None,
226 });
227 }
228
229 return Ok(ModificationResult {
230 changed: true,
231 modified_nodes: vec![backup_node],
232 unmatched_qualified_paths: None,
233 });
234 }
235
236 let literal_default = op.literal_default.as_ref().unwrap();
240
241 let has_type = op.field_def.contains(':');
244
245 let mut def_modified = false;
246 if has_type {
247 let backup_node = BackupNode {
249 node_type: "struct".to_string(),
250 identifier: op.struct_name.clone(),
251 original_content: self.unparse_item(&Item::Struct(item_struct.clone())),
252 location: self.span_to_location(item_struct.span()),
253 };
254
255 def_modified = self.insert_struct_field(&item_struct, op)
257 .context("Failed to add field to struct definition")?;
258
259 if def_modified {
260 modified_nodes.push(backup_node);
261 self.syntax_tree = syn::parse_str(&self.content)
263 .context("Failed to re-parse content after adding struct field")?;
264 self.line_offsets = Self::compute_line_offsets(&self.content);
265 }
266 }
267
268 let field_name = op.field_def.split(':')
271 .next()
272 .map(|s| s.trim().to_string())
273 .context("Failed to extract field name from field definition")?;
274
275 let literal_op = AddStructLiteralFieldOp {
277 struct_name: op.struct_name.clone(),
278 field_def: format!("{}: {}", field_name, literal_default),
279 position: op.position.clone(),
280 struct_path: None, };
282
283 let literal_result = self.add_struct_literal_field(&literal_op)
285 .context("Failed to update struct literals")?;
286 modified_nodes.extend(literal_result.modified_nodes);
287
288 Ok(ModificationResult {
289 changed: true,
290 modified_nodes,
291 unmatched_qualified_paths: None,
292 })
293 }
294
295 fn insert_struct_field(&mut self, item_struct: &ItemStruct, op: &AddStructFieldOp) -> Result<bool> {
296 if let Fields::Named(ref fields) = item_struct.fields {
297 let field_code = format!("struct Dummy {{ {} }}", op.field_def);
299 let dummy: ItemStruct = parse_str(&field_code)
300 .context("Failed to parse field definition")?;
301
302 let new_field = if let Fields::Named(ref nf) = dummy.fields {
303 nf.named.first()
304 .context("No field found in definition")?
305 .clone()
306 } else {
307 anyhow::bail!("Expected named field");
308 };
309
310 let new_field_name = new_field.ident.as_ref()
312 .map(|i| i.to_string())
313 .context("Field must have a name")?;
314
315 if fields.named.iter().any(|f| {
316 f.ident.as_ref().map(|i| i.to_string()) == Some(new_field_name.clone())
317 }) {
318 return Ok(false);
320 }
321
322 let insert_pos = match &op.position {
324 InsertPosition::First => {
325 if let Some(first_field) = fields.named.first() {
326 self.span_to_byte_offset(first_field.span().start())
327 } else {
328 let brace_pos = self.span_to_byte_offset(fields.brace_token.span.join().start());
330 brace_pos + 1
331 }
332 }
333 InsertPosition::Last => {
334 if let Some(last_field) = fields.named.last() {
335 let end = self.span_to_byte_offset(last_field.span().end());
336 self.find_after_field_end(end)
338 } else {
339 let brace_pos = self.span_to_byte_offset(fields.brace_token.span.join().start());
341 brace_pos + 1
342 }
343 }
344 InsertPosition::After(name) => {
345 let field = fields.named.iter()
346 .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(name.clone()))
347 .with_context(|| format!("Field '{}' not found", name))?;
348 let end = self.span_to_byte_offset(field.span().end());
349 self.find_after_field_end(end)
350 }
351 InsertPosition::Before(name) => {
352 let field = fields.named.iter()
353 .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(name.clone()))
354 .with_context(|| format!("Field '{}' not found", name))?;
355 self.span_to_byte_offset(field.span().start())
356 }
357 };
358
359 let indent = self.get_indentation(insert_pos);
361 let field_str = Self::format_field(&new_field);
362 let insert_text = if matches!(op.position, InsertPosition::First) {
363 format!("\n{}{},", indent, field_str)
364 } else {
365 format!("\n{}{},", indent, field_str)
366 };
367
368 self.content.insert_str(insert_pos, &insert_text);
369 return Ok(true);
370 }
371
372 anyhow::bail!("Struct '{}' does not have named fields", op.struct_name)
373 }
374
375 pub(crate) fn update_struct_field(&mut self, op: &UpdateStructFieldOp) -> Result<ModificationResult> {
376 let is_enum_variant = op.struct_name.contains("::");
378
379 if is_enum_variant {
382 anyhow::bail!(
383 "Cannot update field in enum variant definition '{}'.\n\
384 To update fields in enum variant struct literals, use the transform command:\n\
385 rs-hack transform --node-type struct-literal --name {} --action replace --with <new_pattern> --paths ... --apply",
386 op.struct_name, op.struct_name
387 );
388 }
389
390 let item_struct = self.syntax_tree.items.iter()
392 .find_map(|item| {
393 if let Item::Struct(s) = item {
394 if s.ident == op.struct_name {
395 return Some(s.clone());
396 }
397 }
398 None
399 })
400 .ok_or_else(|| anyhow::anyhow!("Struct '{}' not found", op.struct_name))?;
401
402 if let Some(ref where_filter) = op.where_filter {
404 if !self.matches_where_filter(&item_struct.attrs, where_filter)? {
405 return Ok(ModificationResult {
407 changed: false,
408 modified_nodes: vec![],
409 unmatched_qualified_paths: None,
410 });
411 }
412 }
413
414 let backup_node = BackupNode {
416 node_type: "struct".to_string(),
417 identifier: op.struct_name.clone(),
418 original_content: self.unparse_item(&Item::Struct(item_struct.clone())),
419 location: self.span_to_location(item_struct.span()),
420 };
421
422 let modified = self.replace_struct_field(&item_struct, op)?;
423
424 Ok(ModificationResult {
425 changed: modified,
426 modified_nodes: if modified { vec![backup_node] } else { vec![] },
427 unmatched_qualified_paths: None,
428 })
429 }
430
431 fn replace_struct_field(&mut self, item_struct: &ItemStruct, op: &UpdateStructFieldOp) -> Result<bool> {
432 if let Fields::Named(ref fields) = item_struct.fields {
433 let field_code = format!("struct Dummy {{ {} }}", op.field_def);
435 let dummy: ItemStruct = parse_str(&field_code)
436 .context("Failed to parse field definition")?;
437
438 let new_field = if let Fields::Named(ref nf) = dummy.fields {
439 nf.named.first()
440 .context("No field found in definition")?
441 .clone()
442 } else {
443 anyhow::bail!("Expected named field");
444 };
445
446 let field_name = new_field.ident.as_ref()
448 .map(|i| i.to_string())
449 .context("Field must have a name")?;
450
451 let existing_field = fields.named.iter()
453 .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(field_name.clone()))
454 .ok_or_else(|| anyhow::anyhow!("Field '{}' not found in struct '{}'", field_name, op.struct_name))?;
455
456 let start = self.span_to_byte_offset(existing_field.span().start());
458 let end = self.span_to_byte_offset(existing_field.span().end());
459
460 let new_field_str = Self::format_field(&new_field);
462
463 self.content.replace_range(start..end, &new_field_str);
465
466 return Ok(true);
467 }
468
469 anyhow::bail!("Struct '{}' does not have named fields", op.struct_name)
470 }
471
472 pub(crate) fn remove_struct_field(&mut self, op: &RemoveStructFieldOp) -> Result<ModificationResult> {
473 let mut modified_nodes = Vec::new();
474 let mut changed = false;
475
476 let is_enum_variant = op.struct_name.contains("::");
478
479 let effective_literal_only = op.literal_only || is_enum_variant;
482
483 if !effective_literal_only {
485 let item_struct = self.syntax_tree.items.iter()
487 .find_map(|item| {
488 if let Item::Struct(s) = item {
489 if s.ident == op.struct_name {
490 return Some(s.clone());
491 }
492 }
493 None
494 })
495 .ok_or_else(|| anyhow::anyhow!("Struct '{}' not found", op.struct_name))?;
496
497 if let Some(ref where_filter) = op.where_filter {
499 if !self.matches_where_filter(&item_struct.attrs, where_filter)? {
500 return Ok(ModificationResult {
502 changed: false,
503 modified_nodes: vec![],
504 unmatched_qualified_paths: None,
505 });
506 }
507 }
508
509 let backup_node = BackupNode {
511 node_type: "struct".to_string(),
512 identifier: op.struct_name.clone(),
513 original_content: self.unparse_item(&Item::Struct(item_struct.clone())),
514 location: self.span_to_location(item_struct.span()),
515 };
516
517 if let Fields::Named(ref fields) = item_struct.fields {
518 let field_to_remove = fields.named.iter()
520 .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(op.field_name.clone()));
521
522 if field_to_remove.is_none() {
524 let field_names: Vec<String> = fields.named.iter()
525 .filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
526 .collect();
527
528 let suggestions = Self::find_similar_fields(&op.field_name, &field_names);
529
530 if suggestions.is_empty() {
531 return Err(anyhow::anyhow!(
532 "Field '{}' not found in struct '{}'\n\nAvailable fields: {}",
533 op.field_name,
534 op.struct_name,
535 field_names.join(", ")
536 ));
537 } else {
538 return Err(anyhow::anyhow!(
539 "Field '{}' not found in struct '{}'\n\nDid you mean one of these?\n - {}\n\nAll available fields: {}",
540 op.field_name,
541 op.struct_name,
542 suggestions.join("\n - "),
543 field_names.join(", ")
544 ));
545 }
546 }
547
548 let field_to_remove = field_to_remove.unwrap();
549
550 let start = self.span_to_byte_offset(field_to_remove.span().start());
552 let mut end = self.span_to_byte_offset(field_to_remove.span().end());
553
554 while end < self.content.len() {
556 match self.content.as_bytes()[end] as char {
557 ',' => {
558 end += 1;
559 if end < self.content.len() && self.content.as_bytes()[end] == b'\n' {
561 end += 1;
562 }
563 break;
564 }
565 ' ' | '\t' => end += 1,
566 '\n' => {
567 end += 1;
568 break;
569 }
570 _ => break,
571 }
572 }
573
574 let mut line_start = start;
576 while line_start > 0 && self.content.as_bytes()[line_start - 1] != b'\n' {
577 line_start -= 1;
578 }
579
580 let before_field = &self.content[line_start..start];
582 if before_field.trim().is_empty() {
583 self.content.replace_range(line_start..end, "");
585 } else {
586 self.content.replace_range(start..end, "");
588 }
589
590 modified_nodes.push(backup_node);
591 changed = true;
592
593 self.syntax_tree = syn::parse_str(&self.content)?;
595 } else {
596 anyhow::bail!("Struct '{}' does not have named fields", op.struct_name)
597 }
598 }
599
600 let literal_backups = self.collect_struct_literal_backups(&op.struct_name, None);
603
604 use syn::visit::Visit;
606
607 struct FieldDeletionFinder<'a> {
608 struct_name: String,
609 field_name: String,
610 deletion_ranges: Vec<(usize, usize)>, unmatched_paths: std::collections::HashMap<String, usize>, editor: &'a RustEditor,
613 }
614
615 impl<'ast, 'a> Visit<'ast> for FieldDeletionFinder<'a> {
616 fn visit_expr_struct(&mut self, node: &'ast syn::ExprStruct) {
617 let matches = if self.struct_name.contains("::") {
619 if self.struct_name.starts_with("*::") {
620 let target_name = &self.struct_name[3..];
621 node.path.segments.last()
622 .map(|seg| seg.ident.to_string() == target_name)
623 .unwrap_or(false)
624 } else {
625 let path_str = node.path.segments.iter()
626 .map(|seg| seg.ident.to_string())
627 .collect::<Vec<_>>()
628 .join("::");
629 path_str == self.struct_name
630 }
631 } else {
632 let matches = node.path.segments.len() == 1
634 && node.path.segments.last()
635 .map(|seg| seg.ident.to_string())
636 .as_ref() == Some(&self.struct_name);
637
638 if !matches && node.path.segments.len() > 1 {
640 if let Some(last_seg) = node.path.segments.last() {
641 if last_seg.ident.to_string() == self.struct_name {
642 let qualified_path = node.path.segments.iter()
643 .map(|seg| seg.ident.to_string())
644 .collect::<Vec<_>>()
645 .join("::");
646 *self.unmatched_paths.entry(qualified_path).or_insert(0) += 1;
647 }
648 }
649 }
650 matches
651 };
652
653 if matches {
654 let field_idx = node.fields.iter().position(|fv| {
656 if let syn::Member::Named(ident) = &fv.member {
657 ident.to_string() == self.field_name
658 } else {
659 false
660 }
661 });
662
663 if let Some(idx) = field_idx {
664 let field = &node.fields[idx];
665 let start = self.editor.span_to_byte_offset(field.span().start());
666 let mut end = self.editor.span_to_byte_offset(field.span().end());
667
668 let content_bytes = self.editor.content.as_bytes();
671 while end < content_bytes.len() {
672 match content_bytes[end] {
673 b',' => {
674 end += 1;
675 if end < content_bytes.len() && content_bytes[end] == b'\n' {
677 end += 1;
678 }
679 break;
680 }
681 b' ' | b'\t' => {
682 end += 1;
683 }
684 _ => break,
685 }
686 }
687
688 let mut line_start = start;
690 while line_start > 0 {
691 let ch = content_bytes[line_start - 1];
692 if ch == b'\n' {
693 break;
694 } else if ch == b' ' || ch == b'\t' {
695 line_start -= 1;
696 } else {
697 break;
698 }
699 }
700
701 self.deletion_ranges.push((line_start, end));
702 }
703 }
704
705 syn::visit::visit_expr_struct(self, node);
706 }
707 }
708
709 let mut finder = FieldDeletionFinder {
710 struct_name: op.struct_name.clone(),
711 field_name: op.field_name.clone(),
712 deletion_ranges: Vec::new(),
713 unmatched_paths: std::collections::HashMap::new(),
714 editor: self,
715 };
716
717 finder.visit_file(&self.syntax_tree);
718
719 let unmatched_hint = if !op.struct_name.contains("::") && !finder.unmatched_paths.is_empty() {
721 Some(finder.unmatched_paths)
722 } else {
723 None
724 };
725
726 if !finder.deletion_ranges.is_empty() {
727 let mut ranges = finder.deletion_ranges;
729 ranges.sort_by_key(|(start, _)| std::cmp::Reverse(*start));
730
731 for (start, end) in ranges {
733 self.content.drain(start..end);
734 }
735
736 self.syntax_tree = syn::parse_str(&self.content)
738 .context("Failed to re-parse after removing struct literal fields")?;
739 self.line_offsets = Self::compute_line_offsets(&self.content);
740
741 modified_nodes.extend(literal_backups);
742 changed = true;
743 }
744
745 Ok(ModificationResult {
746 changed,
747 modified_nodes,
748 unmatched_qualified_paths: unmatched_hint,
749 })
750 }
751
752 pub(crate) fn add_struct_literal_field(&mut self, op: &AddStructLiteralFieldOp) -> Result<ModificationResult> {
753 let field_name = op.field_def.split(':')
755 .next()
756 .map(|s| s.trim().to_string())
757 .context("Field definition must contain ':'")?;
758
759 let path_resolver = if let Some(struct_path) = &op.struct_path {
761 let mut resolver = PathResolver::new(struct_path)
762 .ok_or_else(|| anyhow::anyhow!("Invalid struct path: {}", struct_path))?;
763
764 resolver.scan_file(&self.syntax_tree);
766 Some(resolver)
767 } else {
768 None
769 };
770
771 let backup_nodes = self.collect_struct_literal_backups(&op.struct_name, path_resolver.as_ref());
773
774 use syn::visit::Visit;
776
777 struct LiteralFieldInserter<'a> {
778 struct_name: String,
779 field_name: String,
780 path_resolver: Option<&'a PathResolver>,
781 insertion_points: Vec<(usize, usize)>, unmatched_paths: std::collections::HashMap<String, usize>, editor: &'a RustEditor,
784 }
785
786 impl<'ast, 'a> Visit<'ast> for LiteralFieldInserter<'a> {
787 fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
788 use syn::parse::Parser;
790
791 if let Ok(exprs) = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated
792 .parse2(node.mac.tokens.clone())
793 {
794 for expr in exprs.iter() {
795 syn::visit::visit_expr(self, expr);
796 }
797 }
798
799 syn::visit::visit_expr_macro(self, node);
800 }
801
802 fn visit_expr_struct(&mut self, node: &'ast syn::ExprStruct) {
803 let is_match = if let Some(resolver) = &self.path_resolver {
805 resolver.matches_target(&node.path)
806 } else {
807 if self.struct_name.contains("::") {
809 if self.struct_name.starts_with("*::") {
810 let target_name = &self.struct_name[3..];
811 node.path.segments.last()
812 .map(|seg| seg.ident.to_string() == target_name)
813 .unwrap_or(false)
814 } else {
815 let path_str = node.path.segments.iter()
816 .map(|seg| seg.ident.to_string())
817 .collect::<Vec<_>>()
818 .join("::");
819 path_str == self.struct_name
820 }
821 } else {
822 let matches = node.path.segments.len() == 1
824 && node.path.segments.last()
825 .map(|seg| seg.ident.to_string())
826 .as_ref() == Some(&self.struct_name);
827
828 if !matches && node.path.segments.len() > 1 {
830 if let Some(last_seg) = node.path.segments.last() {
831 if last_seg.ident.to_string() == self.struct_name {
832 let qualified_path = node.path.segments.iter()
833 .map(|seg| seg.ident.to_string())
834 .collect::<Vec<_>>()
835 .join("::");
836 *self.unmatched_paths.entry(qualified_path).or_insert(0) += 1;
837 }
838 }
839 }
840 matches
841 }
842 };
843
844 if is_match {
845 let field_exists = node.fields.iter().any(|fv| {
847 fv.member.to_token_stream().to_string() == self.field_name
848 });
849
850 if !field_exists {
851 let insert_offset = if let Some(last_field) = node.fields.last() {
853 self.editor.span_to_byte_offset(last_field.span().end())
855 } else {
856 let brace_pos = self.editor.span_to_byte_offset(node.brace_token.span.join().start());
858 brace_pos + 1 };
860
861 let indent = if let Some(last_field) = node.fields.last() {
863 let line_start = self.editor.span_to_byte_offset(last_field.span().start());
864 self.editor.get_indentation(line_start).len()
865 } else {
866 let struct_start = self.editor.span_to_byte_offset(node.span().start());
868 self.editor.get_indentation(struct_start).len() + 4
869 };
870
871 self.insertion_points.push((insert_offset, indent));
872 }
873 }
874
875 syn::visit::visit_expr_struct(self, node);
876 }
877 }
878
879 let mut inserter = LiteralFieldInserter {
880 struct_name: op.struct_name.clone(),
881 field_name: field_name.clone(),
882 path_resolver: path_resolver.as_ref(),
883 insertion_points: Vec::new(),
884 unmatched_paths: std::collections::HashMap::new(),
885 editor: self,
886 };
887
888 inserter.visit_file(&self.syntax_tree);
889
890 let unmatched_hint = if !op.struct_name.contains("::") && !inserter.unmatched_paths.is_empty() {
892 Some(inserter.unmatched_paths)
893 } else {
894 None
895 };
896
897 if inserter.insertion_points.is_empty() {
898 return Ok(ModificationResult {
899 changed: false,
900 modified_nodes: vec![],
901 unmatched_qualified_paths: unmatched_hint,
902 });
903 }
904
905 let mut points = inserter.insertion_points;
908 points.sort_by_key(|(offset, _)| std::cmp::Reverse(*offset));
909
910 for (insert_offset, indent_spaces) in points {
912 let indent = " ".repeat(indent_spaces);
913 let field_str = format!(",\n{}{}", indent, op.field_def);
914 self.content.insert_str(insert_offset, &field_str);
915 }
916
917 self.syntax_tree = syn::parse_str(&self.content)
919 .context("Failed to re-parse after adding struct literal fields")?;
920 self.line_offsets = Self::compute_line_offsets(&self.content);
921
922 Ok(ModificationResult {
923 changed: true,
924 modified_nodes: backup_nodes,
925 unmatched_qualified_paths: unmatched_hint,
926 })
927 }
928
929 pub(crate) fn set_struct_literal_base(&mut self, op: &SetStructLiteralBaseOp) -> Result<ModificationResult> {
932 let base_expr = if op.base_expr == "default" {
934 "Default::default()".to_string()
935 } else {
936 op.base_expr.clone()
937 };
938
939 let path_resolver = if let Some(struct_path) = &op.struct_path {
941 let mut resolver = PathResolver::new(struct_path)
942 .ok_or_else(|| anyhow::anyhow!("Invalid struct path: {}", struct_path))?;
943 resolver.scan_file(&self.syntax_tree);
944 Some(resolver)
945 } else {
946 None
947 };
948
949 let backup_nodes = self.collect_struct_literal_backups(&op.struct_name, path_resolver.as_ref());
951
952 use syn::visit::Visit;
954
955 struct BaseInserter<'a> {
956 struct_name: String,
957 path_resolver: Option<&'a PathResolver>,
958 insertion_points: Vec<(usize, usize, usize)>,
960 unmatched_paths: std::collections::HashMap<String, usize>,
961 editor: &'a RustEditor,
962 }
963
964 impl<'ast, 'a> Visit<'ast> for BaseInserter<'a> {
965 fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
966 use syn::parse::Parser;
967 if let Ok(exprs) = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated
968 .parse2(node.mac.tokens.clone())
969 {
970 for expr in exprs.iter() {
971 syn::visit::visit_expr(self, expr);
972 }
973 }
974 syn::visit::visit_expr_macro(self, node);
975 }
976
977 fn visit_expr_struct(&mut self, node: &'ast syn::ExprStruct) {
978 if node.rest.is_some() {
980 syn::visit::visit_expr_struct(self, node);
981 return;
982 }
983
984 let is_match = if let Some(resolver) = &self.path_resolver {
986 resolver.matches_target(&node.path)
987 } else {
988 if self.struct_name.contains("::") {
990 if self.struct_name.starts_with("*::") {
991 let target_name = &self.struct_name[3..];
992 node.path.segments.last()
993 .map(|seg| seg.ident.to_string() == target_name)
994 .unwrap_or(false)
995 } else {
996 let path_str = node.path.segments.iter()
997 .map(|seg| seg.ident.to_string())
998 .collect::<Vec<_>>()
999 .join("::");
1000 path_str == self.struct_name
1001 }
1002 } else {
1003 let matches = node.path.segments.len() == 1
1004 && node.path.segments.last()
1005 .map(|seg| seg.ident.to_string())
1006 .as_ref() == Some(&self.struct_name);
1007
1008 if !matches && node.path.segments.len() > 1 {
1009 if let Some(last_seg) = node.path.segments.last() {
1010 if last_seg.ident.to_string() == self.struct_name {
1011 let qualified_path = node.path.segments.iter()
1012 .map(|seg| seg.ident.to_string())
1013 .collect::<Vec<_>>()
1014 .join("::");
1015 *self.unmatched_paths.entry(qualified_path).or_insert(0) += 1;
1016 }
1017 }
1018 }
1019 matches
1020 }
1021 };
1022
1023 if is_match {
1024 let brace_span = node.brace_token.span.join();
1026 let close_brace_offset = self.editor.span_to_byte_offset(brace_span.end()) - 1;
1027
1028 let (insert_after, indent) = if let Some(last_field) = node.fields.last() {
1029 let field_end = self.editor.span_to_byte_offset(last_field.span().end());
1030 let field_start_offset = self.editor.span_to_byte_offset(last_field.span().start());
1031 let indent = self.editor.get_indentation(field_start_offset).len();
1032 (field_end, indent)
1033 } else {
1034 let open_brace = self.editor.span_to_byte_offset(brace_span.start()) + 1;
1036 let struct_start = self.editor.span_to_byte_offset(node.span().start());
1037 let indent = self.editor.get_indentation(struct_start).len() + 4;
1038 (open_brace, indent)
1039 };
1040
1041 self.insertion_points.push((insert_after, close_brace_offset, indent));
1042 }
1043
1044 syn::visit::visit_expr_struct(self, node);
1045 }
1046 }
1047
1048 let mut inserter = BaseInserter {
1049 struct_name: op.struct_name.clone(),
1050 path_resolver: path_resolver.as_ref(),
1051 insertion_points: Vec::new(),
1052 unmatched_paths: std::collections::HashMap::new(),
1053 editor: self,
1054 };
1055
1056 inserter.visit_file(&self.syntax_tree);
1057
1058 let unmatched_hint = if !op.struct_name.contains("::") && !inserter.unmatched_paths.is_empty() {
1059 Some(inserter.unmatched_paths)
1060 } else {
1061 None
1062 };
1063
1064 if inserter.insertion_points.is_empty() {
1065 return Ok(ModificationResult {
1066 changed: false,
1067 modified_nodes: vec![],
1068 unmatched_qualified_paths: unmatched_hint,
1069 });
1070 }
1071
1072 let mut points = inserter.insertion_points;
1074 points.sort_by_key(|(insert_after, _, _)| std::cmp::Reverse(*insert_after));
1075
1076 for (insert_after, close_brace_offset, indent_spaces) in points {
1078 let after_insert = &self.content[insert_after..close_brace_offset];
1080 let trimmed_after = after_insert.trim_start();
1081 let has_trailing_comma = trimmed_after.starts_with(',');
1082
1083 let actual_insert_pos = if has_trailing_comma {
1085 let comma_offset = after_insert.find(',').unwrap();
1087 insert_after + comma_offset + 1
1088 } else {
1089 insert_after
1090 };
1091
1092 let between = &self.content[actual_insert_pos..close_brace_offset];
1094 let is_multiline = between.contains('\n');
1095
1096 let before_insert = &self.content[..insert_after];
1098 let trimmed = before_insert.trim_end();
1099 let after_open_brace = trimmed.ends_with('{');
1100
1101 let base_str = if is_multiline {
1102 let indent = " ".repeat(indent_spaces);
1104 if after_open_brace || has_trailing_comma {
1105 format!("\n{}..{}", indent, base_expr)
1107 } else {
1108 format!(",\n{}..{}", indent, base_expr)
1110 }
1111 } else {
1112 if after_open_brace || has_trailing_comma {
1114 format!(" ..{}", base_expr)
1115 } else {
1116 format!(", ..{}", base_expr)
1117 }
1118 };
1119
1120 self.content.insert_str(actual_insert_pos, &base_str);
1121 }
1122
1123 self.syntax_tree = syn::parse_str(&self.content)
1125 .context("Failed to re-parse after adding struct literal base")?;
1126 self.line_offsets = Self::compute_line_offsets(&self.content);
1127
1128 Ok(ModificationResult {
1129 changed: true,
1130 modified_nodes: backup_nodes,
1131 unmatched_qualified_paths: unmatched_hint,
1132 })
1133 }
1134
1135 fn collect_struct_literal_backups(&self, struct_name: &str, path_resolver: Option<&PathResolver>) -> Vec<BackupNode> {
1137 use syn::visit::Visit;
1138 use syn::spanned::Spanned;
1139
1140 struct LiteralCollector<'a> {
1141 struct_name: String,
1142 path_resolver: Option<&'a PathResolver>,
1143 backups: Vec<BackupNode>,
1144 counter: usize,
1145 editor: &'a RustEditor,
1146 }
1147
1148 impl<'ast, 'a> Visit<'ast> for LiteralCollector<'a> {
1149 fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
1150 use syn::parse::Parser;
1152
1153 if let Ok(exprs) = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated
1154 .parse2(node.mac.tokens.clone())
1155 {
1156 for expr in exprs.iter() {
1157 syn::visit::visit_expr(self, expr);
1158 }
1159 }
1160
1161 syn::visit::visit_expr_macro(self, node);
1162 }
1163
1164 fn visit_expr(&mut self, node: &'ast Expr) {
1165 if let Expr::Struct(expr_struct) = node {
1166 let matches = if let Some(resolver) = self.path_resolver {
1167 resolver.matches_target(&expr_struct.path)
1169 } else {
1170 if self.struct_name.contains("::") {
1176 if self.struct_name.starts_with("*::") {
1178 let target_name = &self.struct_name[3..]; expr_struct.path.segments.last()
1181 .map(|seg| seg.ident.to_string() == target_name)
1182 .unwrap_or(false)
1183 } else {
1184 let path_str = expr_struct.path.segments.iter()
1186 .map(|seg| seg.ident.to_string())
1187 .collect::<Vec<_>>()
1188 .join("::");
1189 path_str == self.struct_name
1190 }
1191 } else {
1192 expr_struct.path.segments.len() == 1
1194 && expr_struct.path.segments.last()
1195 .map(|seg| seg.ident.to_string() == self.struct_name)
1196 .unwrap_or(false)
1197 }
1198 };
1199
1200 if matches {
1201 let start = self.editor.span_to_byte_offset(expr_struct.span().start());
1203 let end = self.editor.span_to_byte_offset(expr_struct.span().end());
1204 let original_source = &self.editor.content[start..end];
1205
1206 self.backups.push(BackupNode {
1207 node_type: "struct-literal".to_string(),
1208 identifier: format!("{}#{}", self.struct_name, self.counter),
1209 original_content: original_source.to_string(),
1210 location: NodeLocation {
1211 line: 0, column: 0,
1213 end_line: 0,
1214 end_column: 0,
1215 },
1216 });
1217 self.counter += 1;
1218 }
1219 }
1220 syn::visit::visit_expr(self, node);
1221 }
1222 }
1223
1224 let mut collector = LiteralCollector {
1225 struct_name: struct_name.to_string(),
1226 path_resolver,
1227 backups: Vec::new(),
1228 counter: 0,
1229 editor: self,
1230 };
1231
1232 collector.visit_file(&self.syntax_tree);
1233 collector.backups
1234 }
1235
1236 pub(crate) fn add_enum_variant(&mut self, op: &AddEnumVariantOp) -> Result<ModificationResult> {
1237 let item_enum = self.syntax_tree.items.iter()
1239 .find_map(|item| {
1240 if let Item::Enum(e) = item {
1241 if e.ident == op.enum_name {
1242 return Some(e.clone());
1243 }
1244 }
1245 None
1246 })
1247 .ok_or_else(|| anyhow::anyhow!("Enum '{}' not found", op.enum_name))?;
1248
1249 if let Some(ref where_filter) = op.where_filter {
1251 if !self.matches_where_filter(&item_enum.attrs, where_filter)? {
1252 return Ok(ModificationResult {
1254 changed: false,
1255 modified_nodes: vec![],
1256 unmatched_qualified_paths: None,
1257 });
1258 }
1259 }
1260
1261 let backup_node = BackupNode {
1263 node_type: "enum".to_string(),
1264 identifier: op.enum_name.clone(),
1265 original_content: self.unparse_item(&Item::Enum(item_enum.clone())),
1266 location: self.span_to_location(item_enum.span()),
1267 };
1268
1269 let modified = self.insert_enum_variant(&item_enum, op)?;
1270
1271 Ok(ModificationResult {
1272 changed: modified,
1273 modified_nodes: if modified { vec![backup_node] } else { vec![] },
1274 unmatched_qualified_paths: None,
1275 })
1276 }
1277
1278 fn insert_enum_variant(&mut self, item_enum: &ItemEnum, op: &AddEnumVariantOp) -> Result<bool> {
1279 let variant_code = format!("enum Dummy {{ {} }}", op.variant_def);
1281 let dummy: ItemEnum = parse_str(&variant_code)
1282 .context("Failed to parse variant definition")?;
1283
1284 let new_variant = dummy.variants.first()
1285 .context("No variant found in definition")?
1286 .clone();
1287
1288 let variant_name = new_variant.ident.to_string();
1290 if item_enum.variants.iter().any(|v| v.ident.to_string() == variant_name) {
1291 return Ok(false);
1293 }
1294
1295 let insert_pos = match &op.position {
1297 InsertPosition::First => {
1298 if let Some(first_var) = item_enum.variants.first() {
1299 self.span_to_byte_offset(first_var.span().start())
1300 } else {
1301 let brace_pos = self.span_to_byte_offset(item_enum.brace_token.span.join().start());
1302 brace_pos + 1
1303 }
1304 }
1305 InsertPosition::Last => {
1306 if let Some(last_var) = item_enum.variants.last() {
1307 let end = self.span_to_byte_offset(last_var.span().end());
1308 self.find_after_field_end(end)
1309 } else {
1310 let brace_pos = self.span_to_byte_offset(item_enum.brace_token.span.join().start());
1311 brace_pos + 1
1312 }
1313 }
1314 InsertPosition::After(name) => {
1315 let variant = item_enum.variants.iter()
1316 .find(|v| v.ident.to_string() == *name)
1317 .with_context(|| format!("Variant '{}' not found", name))?;
1318 let end = self.span_to_byte_offset(variant.span().end());
1319 self.find_after_field_end(end)
1320 }
1321 InsertPosition::Before(name) => {
1322 let variant = item_enum.variants.iter()
1323 .find(|v| v.ident.to_string() == *name)
1324 .with_context(|| format!("Variant '{}' not found", name))?;
1325 self.span_to_byte_offset(variant.span().start())
1326 }
1327 };
1328
1329 let indent = self.get_indentation(insert_pos);
1330 let variant_str = new_variant.to_token_stream().to_string();
1331 let insert_text = format!("\n{}{},", indent, variant_str);
1332
1333 self.content.insert_str(insert_pos, &insert_text);
1334 Ok(true)
1335 }
1336
1337 fn update_enum_variant(&mut self, op: &UpdateEnumVariantOp) -> Result<ModificationResult> {
1338 let item_enum = self.syntax_tree.items.iter()
1340 .find_map(|item| {
1341 if let Item::Enum(e) = item {
1342 if e.ident == op.enum_name {
1343 return Some(e.clone());
1344 }
1345 }
1346 None
1347 })
1348 .ok_or_else(|| anyhow::anyhow!("Enum '{}' not found", op.enum_name))?;
1349
1350 if let Some(ref where_filter) = op.where_filter {
1352 if !self.matches_where_filter(&item_enum.attrs, where_filter)? {
1353 return Ok(ModificationResult {
1355 changed: false,
1356 modified_nodes: vec![],
1357 unmatched_qualified_paths: None,
1358 });
1359 }
1360 }
1361
1362 let backup_node = BackupNode {
1364 node_type: "enum".to_string(),
1365 identifier: op.enum_name.clone(),
1366 original_content: self.unparse_item(&Item::Enum(item_enum.clone())),
1367 location: self.span_to_location(item_enum.span()),
1368 };
1369
1370 let variant_code = format!("enum Dummy {{ {} }}", op.variant_def);
1372 let dummy: ItemEnum = parse_str(&variant_code)
1373 .context("Failed to parse variant definition")?;
1374
1375 let new_variant = dummy.variants.first()
1376 .context("No variant found in definition")?
1377 .clone();
1378
1379 let variant_name = new_variant.ident.to_string();
1380
1381 let existing_variant = item_enum.variants.iter()
1383 .find(|v| v.ident.to_string() == variant_name)
1384 .ok_or_else(|| anyhow::anyhow!("Variant '{}' not found in enum '{}'", variant_name, op.enum_name))?;
1385
1386 let start = self.span_to_byte_offset(existing_variant.span().start());
1388 let end = self.span_to_byte_offset(existing_variant.span().end());
1389
1390 let variant_str = new_variant.to_token_stream().to_string();
1392 self.content.replace_range(start..end, &variant_str);
1393
1394 Ok(ModificationResult {
1395 changed: true,
1396 modified_nodes: vec![backup_node],
1397 unmatched_qualified_paths: None,
1398 })
1399 }
1400
1401 pub(crate) fn remove_enum_variant(&mut self, op: &RemoveEnumVariantOp) -> Result<ModificationResult> {
1402 let item_enum = self.syntax_tree.items.iter()
1404 .find_map(|item| {
1405 if let Item::Enum(e) = item {
1406 if e.ident == op.enum_name {
1407 return Some(e.clone());
1408 }
1409 }
1410 None
1411 })
1412 .ok_or_else(|| anyhow::anyhow!("Enum '{}' not found", op.enum_name))?;
1413
1414 if let Some(ref where_filter) = op.where_filter {
1416 if !self.matches_where_filter(&item_enum.attrs, where_filter)? {
1417 return Ok(ModificationResult {
1419 changed: false,
1420 modified_nodes: vec![],
1421 unmatched_qualified_paths: None,
1422 });
1423 }
1424 }
1425
1426 let backup_node = BackupNode {
1428 node_type: "enum".to_string(),
1429 identifier: op.enum_name.clone(),
1430 original_content: self.unparse_item(&Item::Enum(item_enum.clone())),
1431 location: self.span_to_location(item_enum.span()),
1432 };
1433
1434 let variant_to_remove = item_enum.variants.iter()
1436 .find(|v| v.ident.to_string() == op.variant_name)
1437 .ok_or_else(|| anyhow::anyhow!("Variant '{}' not found in enum '{}'", op.variant_name, op.enum_name))?;
1438
1439 let start = self.span_to_byte_offset(variant_to_remove.span().start());
1441 let mut end = self.span_to_byte_offset(variant_to_remove.span().end());
1442
1443 while end < self.content.len() {
1445 match self.content.as_bytes()[end] as char {
1446 ',' => {
1447 end += 1;
1448 if end < self.content.len() && self.content.as_bytes()[end] == b'\n' {
1449 end += 1;
1450 }
1451 break;
1452 }
1453 ' ' | '\t' => end += 1,
1454 '\n' => {
1455 end += 1;
1456 break;
1457 }
1458 _ => break,
1459 }
1460 }
1461
1462 let mut line_start = start;
1464 while line_start > 0 && self.content.as_bytes()[line_start - 1] != b'\n' {
1465 line_start -= 1;
1466 }
1467
1468 let before_variant = &self.content[line_start..start];
1469 if before_variant.trim().is_empty() {
1470 self.content.replace_range(line_start..end, "");
1471 } else {
1472 self.content.replace_range(start..end, "");
1473 }
1474
1475 Ok(ModificationResult {
1476 changed: true,
1477 modified_nodes: vec![backup_node],
1478 unmatched_qualified_paths: None,
1479 })
1480 }
1481
1482 pub(crate) fn add_match_arm(&mut self, op: &AddMatchArmOp) -> Result<ModificationResult> {
1483 if op.auto_detect {
1484 self.add_missing_match_arms(op)
1486 } else {
1487 self.add_single_match_arm(op)
1489 }
1490 }
1491
1492 fn add_single_match_arm(&mut self, op: &AddMatchArmOp) -> Result<ModificationResult> {
1493 let dummy_match = format!("match () {{ {} => {}, }}", op.pattern, op.body);
1495 let expr: syn::Expr = parse_str(&dummy_match)
1496 .with_context(|| format!("Failed to parse pattern/body: {} => {}", op.pattern, op.body))?;
1497
1498 let arm = if let syn::Expr::Match(match_expr) = expr {
1500 match_expr.arms.into_iter().next()
1501 .context("Failed to extract arm from dummy match")?
1502 } else {
1503 anyhow::bail!("Expected match expression");
1504 };
1505
1506 let backup_node = if let Some(ref fn_name) = op.function_name {
1508 self.get_function_backup(fn_name)?
1509 } else {
1510 BackupNode {
1513 node_type: "Unknown".to_string(),
1514 identifier: "match_expression".to_string(),
1515 original_content: String::new(),
1516 location: NodeLocation {
1517 line: 0,
1518 column: 0,
1519 end_line: 0,
1520 end_column: 0,
1521 },
1522 }
1523 };
1524
1525 let mut visitor = MatchArmAdder {
1527 target_function: op.function_name.clone(),
1528 arm_to_add: arm,
1529 modified: false,
1530 current_function: None,
1531 modified_function: None,
1532 };
1533
1534 visitor.visit_file_mut(&mut self.syntax_tree);
1535
1536 if visitor.modified {
1537 self.replace_modified_functions(&visitor.modified_function)?;
1539 Ok(ModificationResult {
1540 changed: true,
1541 modified_nodes: vec![backup_node],
1542 unmatched_qualified_paths: None,
1543 })
1544 } else {
1545 Ok(ModificationResult {
1546 changed: false,
1547 modified_nodes: vec![],
1548 unmatched_qualified_paths: None,
1549 })
1550 }
1551 }
1552
1553 fn unparse_item(&self, item: &Item) -> String {
1555 let temp_file = syn::File {
1556 shebang: None,
1557 attrs: Vec::new(),
1558 items: vec![item.clone()],
1559 };
1560 prettyplease::unparse(&temp_file).trim().to_string()
1561 }
1562
1563 fn reformat_item_isolated<F>(&mut self, predicate: F) -> Result<bool>
1566 where
1567 F: Fn(&Item) -> bool,
1568 {
1569 let original_syntax_tree: syn::File = syn::parse_str(&self.content)
1571 .context("Failed to parse original content")?;
1572
1573 let (item_index, original_item) = original_syntax_tree.items.iter()
1574 .enumerate()
1575 .find(|(_, item)| predicate(item))
1576 .ok_or_else(|| anyhow::anyhow!("Item not found"))?;
1577
1578 let start = self.span_to_byte_offset(original_item.span().start());
1580 let end = self.span_to_byte_offset(original_item.span().end());
1581
1582 if item_index >= self.syntax_tree.items.len() {
1584 anyhow::bail!("Item index out of bounds after modification");
1585 }
1586 let modified_item = &self.syntax_tree.items[item_index];
1587
1588 let formatted_item = self.unparse_item(modified_item);
1590
1591 self.content.replace_range(start..end, &formatted_item);
1593
1594 self.syntax_tree = syn::parse_str(&self.content)
1596 .context("Failed to re-parse after isolated prettyplease")?;
1597 self.line_offsets = Self::compute_line_offsets(&self.content);
1598
1599 Ok(true)
1600 }
1601
1602 fn get_function_backup(&self, fn_name: &str) -> Result<BackupNode> {
1604 for item in &self.syntax_tree.items {
1605 if let Item::Fn(f) = item {
1606 if f.sig.ident == fn_name {
1607 return Ok(BackupNode {
1608 node_type: "function".to_string(),
1609 identifier: fn_name.to_string(),
1610 original_content: self.unparse_item(&Item::Fn(f.clone())),
1611 location: self.span_to_location(f.span()),
1612 });
1613 }
1614 }
1615 }
1616 anyhow::bail!("Function '{}' not found", fn_name)
1617 }
1618
1619 fn add_missing_match_arms(&mut self, op: &AddMatchArmOp) -> Result<ModificationResult> {
1620 let enum_name = op.enum_name.as_ref()
1622 .ok_or_else(|| anyhow::anyhow!("enum_name is required for auto-detect"))?;
1623
1624 let enum_variants = self.find_enum_variants(enum_name)?;
1626
1627 if enum_variants.is_empty() {
1628 anyhow::bail!("Enum '{}' not found or has no variants", enum_name);
1629 }
1630
1631 let existing_patterns = self.find_existing_match_patterns(&op.function_name);
1633
1634 let mut missing_variants = Vec::new();
1636 for variant in &enum_variants {
1637 let pattern = format!("{}::{}", enum_name, variant);
1638 let pattern_normalized = pattern.replace(" ", "");
1639
1640 let exists = existing_patterns.iter().any(|p| {
1641 p.replace(" ", "") == pattern_normalized
1642 });
1643
1644 if !exists {
1645 missing_variants.push(variant.clone());
1646 }
1647 }
1648
1649 if missing_variants.is_empty() {
1650 println!("All enum variants already covered in match expressions");
1651 return Ok(ModificationResult {
1652 changed: false,
1653 modified_nodes: vec![],
1654 unmatched_qualified_paths: None,
1655 });
1656 }
1657
1658 let backup_node = if let Some(ref fn_name) = op.function_name {
1660 self.get_function_backup(fn_name)?
1661 } else {
1662 BackupNode {
1663 node_type: "Unknown".to_string(),
1664 identifier: "match_expression".to_string(),
1665 original_content: String::new(),
1666 location: NodeLocation {
1667 line: 0,
1668 column: 0,
1669 end_line: 0,
1670 end_column: 0,
1671 },
1672 }
1673 };
1674
1675 let mut arms_to_add = Vec::new();
1677 for variant in &missing_variants {
1678 let pattern = format!("{}::{}", enum_name, variant);
1679 let dummy_match = format!("match () {{ {} => {}, }}", pattern, op.body);
1680 let expr: syn::Expr = parse_str(&dummy_match)
1681 .with_context(|| format!("Failed to parse pattern/body: {} => {}", pattern, op.body))?;
1682
1683 if let syn::Expr::Match(match_expr) = expr {
1684 if let Some(arm) = match_expr.arms.into_iter().next() {
1685 arms_to_add.push((pattern.clone(), arm));
1686 }
1687 }
1688 }
1689
1690 let mut visitor = MultiMatchArmAdder {
1692 target_function: op.function_name.clone(),
1693 arms_to_add,
1694 modified: false,
1695 current_function: None,
1696 modified_function: None,
1697 };
1698
1699 visitor.visit_file_mut(&mut self.syntax_tree);
1700
1701 if visitor.modified {
1702 for variant in &missing_variants {
1704 println!("Added match arm for: {}::{}", enum_name, variant);
1705 }
1706
1707 self.replace_modified_functions(&visitor.modified_function)?;
1709 Ok(ModificationResult {
1710 changed: true,
1711 modified_nodes: vec![backup_node],
1712 unmatched_qualified_paths: None,
1713 })
1714 } else {
1715 Ok(ModificationResult {
1716 changed: false,
1717 modified_nodes: vec![],
1718 unmatched_qualified_paths: None,
1719 })
1720 }
1721 }
1722
1723 fn find_enum_variants(&self, enum_name: &str) -> Result<Vec<String>> {
1724 for item in &self.syntax_tree.items {
1726 if let Item::Enum(e) = item {
1727 if e.ident == enum_name {
1728 let variants: Vec<String> = e.variants.iter()
1729 .map(|v| v.ident.to_string())
1730 .collect();
1731 return Ok(variants);
1732 }
1733 }
1734 }
1735
1736 Ok(Vec::new())
1737 }
1738
1739 fn find_existing_match_patterns(&self, function_name: &Option<String>) -> Vec<String> {
1740 use syn::visit::Visit;
1741
1742 struct PatternCollector {
1743 target_function: Option<String>,
1744 current_function: Option<String>,
1745 patterns: Vec<String>,
1746 }
1747
1748 impl<'ast> Visit<'ast> for PatternCollector {
1749 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
1750 let prev_fn = self.current_function.clone();
1751 self.current_function = Some(node.sig.ident.to_string());
1752 syn::visit::visit_item_fn(self, node);
1753 self.current_function = prev_fn;
1754 }
1755
1756 fn visit_expr_match(&mut self, node: &'ast ExprMatch) {
1757 if let Some(ref target) = self.target_function {
1759 if self.current_function.as_ref() != Some(target) {
1760 syn::visit::visit_expr_match(self, node);
1761 return;
1762 }
1763 }
1764
1765 for arm in &node.arms {
1767 self.patterns.push(arm.pat.to_token_stream().to_string());
1768 }
1769
1770 syn::visit::visit_expr_match(self, node);
1771 }
1772 }
1773
1774 let mut collector = PatternCollector {
1775 target_function: function_name.clone(),
1776 current_function: None,
1777 patterns: Vec::new(),
1778 };
1779
1780 collector.visit_file(&self.syntax_tree);
1781 collector.patterns
1782 }
1783
1784 pub(crate) fn update_match_arm(&mut self, op: &UpdateMatchArmOp) -> Result<ModificationResult> {
1785 let backup_node = if let Some(ref fn_name) = op.function_name {
1787 self.get_function_backup(fn_name)?
1788 } else {
1789 BackupNode {
1790 node_type: "Unknown".to_string(),
1791 identifier: "match_expression".to_string(),
1792 original_content: String::new(),
1793 location: NodeLocation {
1794 line: 0,
1795 column: 0,
1796 end_line: 0,
1797 end_column: 0,
1798 },
1799 }
1800 };
1801
1802 let new_body: syn::Expr = parse_str(&op.new_body)
1804 .with_context(|| format!("Failed to parse new body: {}", op.new_body))?;
1805
1806 let mut visitor = MatchArmUpdater {
1808 target_function: op.function_name.clone(),
1809 pattern_to_match: op.pattern.clone(),
1810 new_body,
1811 modified: false,
1812 current_function: None,
1813 modified_function: None,
1814 };
1815
1816 visitor.visit_file_mut(&mut self.syntax_tree);
1817
1818 if visitor.modified {
1819 self.replace_modified_functions(&visitor.modified_function)?;
1821 Ok(ModificationResult {
1822 changed: true,
1823 modified_nodes: vec![backup_node],
1824 unmatched_qualified_paths: None,
1825 })
1826 } else {
1827 anyhow::bail!("Pattern '{}' not found in any match expression", op.pattern)
1828 }
1829 }
1830
1831 pub(crate) fn remove_match_arm(&mut self, op: &RemoveMatchArmOp) -> Result<ModificationResult> {
1832 let backup_node = if let Some(ref fn_name) = op.function_name {
1834 self.get_function_backup(fn_name)?
1835 } else {
1836 BackupNode {
1837 node_type: "Unknown".to_string(),
1838 identifier: "match_expression".to_string(),
1839 original_content: String::new(),
1840 location: NodeLocation {
1841 line: 0,
1842 column: 0,
1843 end_line: 0,
1844 end_column: 0,
1845 },
1846 }
1847 };
1848
1849 let mut visitor = MatchArmRemover {
1851 target_function: op.function_name.clone(),
1852 pattern_to_remove: op.pattern.clone(),
1853 modified: false,
1854 current_function: None,
1855 modified_function: None,
1856 };
1857
1858 visitor.visit_file_mut(&mut self.syntax_tree);
1859
1860 if visitor.modified {
1861 self.replace_modified_functions(&visitor.modified_function)?;
1863 Ok(ModificationResult {
1864 changed: true,
1865 modified_nodes: vec![backup_node],
1866 unmatched_qualified_paths: None,
1867 })
1868 } else {
1869 anyhow::bail!("Pattern '{}' not found in any match expression", op.pattern)
1870 }
1871 }
1872
1873 pub(crate) fn add_impl_method(&mut self, op: &AddImplMethodOp) -> Result<ModificationResult> {
1874 let method_code = format!("impl Dummy {{ {} }}", op.method_def);
1876 let dummy: syn::ItemImpl = parse_str(&method_code)
1877 .context("Failed to parse method definition")?;
1878
1879 let new_method = dummy.items.first()
1880 .context("No method found in definition")?
1881 .clone();
1882
1883 let method_name = match &new_method {
1885 syn::ImplItem::Fn(f) => f.sig.ident.to_string(),
1886 _ => anyhow::bail!("Only method definitions are supported"),
1887 };
1888
1889 let impl_index = self.syntax_tree.items.iter().position(|item| {
1891 if let Item::Impl(impl_block) = item {
1892 if let syn::Type::Path(type_path) = &*impl_block.self_ty {
1894 if let Some(segment) = type_path.path.segments.last() {
1895 return segment.ident == op.target;
1896 }
1897 }
1898 }
1899 false
1900 }).ok_or_else(|| anyhow::anyhow!("impl block for '{}' not found", op.target))?;
1901
1902 let impl_block = match &self.syntax_tree.items[impl_index] {
1904 Item::Impl(i) => i,
1905 _ => unreachable!(),
1906 };
1907
1908 let method_exists = impl_block.items.iter().any(|item| {
1909 if let syn::ImplItem::Fn(f) = item {
1910 f.sig.ident == method_name
1911 } else {
1912 false
1913 }
1914 });
1915
1916 if method_exists {
1917 return Ok(ModificationResult {
1918 changed: false,
1919 modified_nodes: vec![],
1920 unmatched_qualified_paths: None,
1921 });
1922 }
1923
1924 let backup_node = BackupNode {
1926 node_type: "ItemImpl".to_string(),
1927 identifier: op.target.clone(),
1928 original_content: self.unparse_item(&self.syntax_tree.items[impl_index].clone()),
1929 location: self.span_to_location(impl_block.span()),
1930 };
1931
1932 let impl_span = impl_block.span();
1934
1935 match &mut self.syntax_tree.items[impl_index] {
1937 Item::Impl(impl_block) => {
1938 match &op.position {
1940 InsertPosition::First => {
1941 impl_block.items.insert(0, new_method);
1942 }
1943 InsertPosition::Last => {
1944 impl_block.items.push(new_method);
1945 }
1946 InsertPosition::After(name) => {
1947 let pos = impl_block.items.iter().position(|item| {
1948 if let syn::ImplItem::Fn(f) = item {
1949 f.sig.ident == name
1950 } else {
1951 false
1952 }
1953 }).with_context(|| format!("Method '{}' not found", name))?;
1954 impl_block.items.insert(pos + 1, new_method);
1955 }
1956 InsertPosition::Before(name) => {
1957 let pos = impl_block.items.iter().position(|item| {
1958 if let syn::ImplItem::Fn(f) = item {
1959 f.sig.ident == name
1960 } else {
1961 false
1962 }
1963 }).with_context(|| format!("Method '{}' not found", name))?;
1964 impl_block.items.insert(pos, new_method);
1965 }
1966 }
1967 }
1968 _ => unreachable!(),
1969 }
1970
1971 self.replace_formatted_item(impl_index, impl_span)?;
1973
1974 Ok(ModificationResult {
1975 changed: true,
1976 modified_nodes: vec![backup_node],
1977 unmatched_qualified_paths: None,
1978 })
1979 }
1980
1981 pub(crate) fn add_use_statement(&mut self, op: &AddUseStatementOp) -> Result<ModificationResult> {
1982 let use_code = format!("use {};", op.use_path);
1984 let use_item: syn::ItemUse = parse_str(&use_code)
1985 .context("Failed to parse use statement")?;
1986
1987 let use_exists = self.syntax_tree.items.iter().any(|item| {
1989 if let Item::Use(existing_use) = item {
1990 existing_use.tree.to_token_stream().to_string() ==
1992 use_item.tree.to_token_stream().to_string()
1993 } else {
1994 false
1995 }
1996 });
1997
1998 if use_exists {
1999 return Ok(ModificationResult {
2000 changed: false,
2001 modified_nodes: vec![],
2002 unmatched_qualified_paths: None,
2003 });
2004 }
2005
2006 let backup_node = BackupNode {
2008 node_type: "ItemUse".to_string(),
2009 identifier: op.use_path.clone(),
2010 original_content: format!("use {};", op.use_path),
2011 location: NodeLocation {
2012 line: 0,
2013 column: 0,
2014 end_line: 0,
2015 end_column: 0,
2016 },
2017 };
2018
2019 let insert_index = match &op.position {
2021 InsertPosition::First => 0,
2022 InsertPosition::Last => {
2023 self.syntax_tree.items.iter()
2025 .rposition(|item| matches!(item, Item::Use(_)))
2026 .map(|i| i + 1)
2027 .unwrap_or(0)
2028 }
2029 InsertPosition::After(path) => {
2030 let pos = self.syntax_tree.items.iter().position(|item| {
2032 if let Item::Use(u) = item {
2033 u.tree.to_token_stream().to_string().contains(path)
2034 } else {
2035 false
2036 }
2037 }).with_context(|| format!("Use statement for '{}' not found", path))?;
2038 pos + 1
2039 }
2040 InsertPosition::Before(path) => {
2041 self.syntax_tree.items.iter().position(|item| {
2043 if let Item::Use(u) = item {
2044 u.tree.to_token_stream().to_string().contains(path)
2045 } else {
2046 false
2047 }
2048 }).with_context(|| format!("Use statement for '{}' not found", path))?
2049 }
2050 };
2051
2052 self.syntax_tree.items.insert(insert_index, Item::Use(use_item));
2054
2055 let insert_line_pos = if insert_index == 0 {
2058 0
2060 } else {
2061 let prev_item = &self.syntax_tree.items[insert_index - 1];
2063 let span = prev_item.span();
2064 let end_pos = self.span_to_byte_offset(span.end());
2065
2066 let mut line_end = end_pos;
2068 while line_end < self.content.len() && self.content.as_bytes()[line_end] != b'\n' {
2069 line_end += 1;
2070 }
2071 if line_end < self.content.len() {
2073 line_end + 1
2074 } else {
2075 self.content.push('\n');
2077 self.content.len()
2078 }
2079 };
2080
2081 let use_str = format!("use {};\n", op.use_path);
2083
2084 self.content.insert_str(insert_line_pos, &use_str);
2086
2087 Ok(ModificationResult {
2088 changed: true,
2089 modified_nodes: vec![backup_node],
2090 unmatched_qualified_paths: None,
2091 })
2092 }
2093
2094 pub(crate) fn add_derive(&mut self, op: &AddDeriveOp) -> Result<ModificationResult> {
2095 let item_index = self.syntax_tree.items.iter().position(|item| {
2097 match (&op.target_type as &str, item) {
2098 ("struct", Item::Struct(s)) => s.ident == op.target_name,
2099 ("enum", Item::Enum(e)) => e.ident == op.target_name,
2100 _ => false,
2101 }
2102 }).ok_or_else(|| anyhow::anyhow!("{} '{}' not found", op.target_type, op.target_name))?;
2103
2104 let (existing_derives, item_span, item_attrs) = match &self.syntax_tree.items[item_index] {
2106 Item::Struct(s) => (Self::extract_derives(&s.attrs), s.span(), &s.attrs),
2107 Item::Enum(e) => (Self::extract_derives(&e.attrs), e.span(), &e.attrs),
2108 _ => (Vec::new(), proc_macro2::Span::call_site(), &Vec::new() as &Vec<syn::Attribute>),
2109 };
2110
2111 if let Some(ref where_filter) = op.where_filter {
2113 if !self.matches_where_filter(item_attrs, where_filter)? {
2114 return Ok(ModificationResult {
2116 changed: false,
2117 modified_nodes: vec![],
2118 unmatched_qualified_paths: None,
2119 });
2120 }
2121 }
2122
2123 let backup_node = BackupNode {
2125 node_type: if op.target_type == "struct" { "struct" } else { "enum" }.to_string(),
2126 identifier: op.target_name.clone(),
2127 original_content: self.unparse_item(&self.syntax_tree.items[item_index].clone()),
2128 location: self.span_to_location(item_span),
2129 };
2130
2131 let new_derives: Vec<String> = op.derives.iter()
2133 .filter(|d| !existing_derives.contains(&d.to_string()))
2134 .cloned()
2135 .collect();
2136
2137 if new_derives.is_empty() {
2138 return Ok(ModificationResult {
2140 changed: false,
2141 modified_nodes: vec![],
2142 unmatched_qualified_paths: None,
2143 });
2144 }
2145
2146 let mut all_derives = existing_derives;
2148 all_derives.extend(new_derives);
2149
2150 let all_derives_refs: Vec<&str> = all_derives.iter().map(|s| s.as_str()).collect();
2152
2153 match &mut self.syntax_tree.items[item_index] {
2155 Item::Struct(s) => {
2156 Self::update_derive_attr(&mut s.attrs, &all_derives_refs)?;
2157 }
2158 Item::Enum(e) => {
2159 Self::update_derive_attr(&mut e.attrs, &all_derives_refs)?;
2160 }
2161 _ => unreachable!(),
2162 }
2163
2164 self.replace_formatted_item(item_index, item_span)?;
2166
2167 Ok(ModificationResult {
2168 changed: true,
2169 modified_nodes: vec![backup_node],
2170 unmatched_qualified_paths: None,
2171 })
2172 }
2173
2174 fn replace_formatted_item(&mut self, item_index: usize, original_span: Span) -> Result<()> {
2176 let item_start_pos = self.span_to_byte_offset(original_span.start());
2178 let item_end_pos = self.span_to_byte_offset(original_span.end());
2179
2180 let mut actual_start = item_start_pos;
2182
2183 let mut temp_pos = item_start_pos;
2185 while temp_pos > 0 {
2186 temp_pos = temp_pos.saturating_sub(1);
2188 let mut line_start = temp_pos;
2189 while line_start > 0 && self.content.as_bytes()[line_start - 1] != b'\n' {
2190 line_start -= 1;
2191 }
2192
2193 let line = if temp_pos < self.content.len() {
2194 &self.content[line_start..temp_pos + 1]
2195 } else {
2196 &self.content[line_start..]
2197 };
2198 let trimmed = line.trim();
2199
2200 if trimmed.starts_with("#[") {
2201 actual_start = line_start;
2202 temp_pos = line_start;
2203 } else if trimmed.is_empty() {
2204 temp_pos = line_start;
2205 } else {
2206 break;
2207 }
2208
2209 if line_start == 0 {
2210 break;
2211 }
2212 }
2213
2214 let item_clone = self.syntax_tree.items[item_index].clone();
2216 let temp_file = syn::File {
2217 shebang: None,
2218 attrs: Vec::new(),
2219 items: vec![item_clone],
2220 };
2221
2222 let formatted = prettyplease::unparse(&temp_file);
2224 let formatted = formatted.trim();
2225
2226 self.content.replace_range(actual_start..item_end_pos, formatted);
2228
2229 Ok(())
2230 }
2231
2232 fn extract_derives(attrs: &[syn::Attribute]) -> Vec<String> {
2234 for attr in attrs {
2235 if attr.path().is_ident("derive") {
2236 if let Ok(syn::Meta::List(meta_list)) = attr.meta.clone().try_into() {
2237 let tokens_str = meta_list.tokens.to_string();
2238 return tokens_str
2239 .split(',')
2240 .map(|s| s.trim().to_string())
2241 .collect();
2242 }
2243 }
2244 }
2245 Vec::new()
2246 }
2247
2248 fn matches_where_filter(&self, attrs: &[syn::Attribute], where_filter: &str) -> Result<bool> {
2253 if let Some(filter_value) = where_filter.strip_prefix("derives_trait:") {
2255 let required_traits: Vec<&str> = filter_value.split(',').map(|s| s.trim()).collect();
2256 let existing_derives = Self::extract_derives(attrs);
2257
2258 for required_trait in required_traits {
2260 if existing_derives.iter().any(|d| d == required_trait) {
2261 return Ok(true);
2262 }
2263 }
2264 return Ok(false);
2265 }
2266
2267 Ok(true)
2269 }
2270
2271 fn update_derive_attr(attrs: &mut Vec<syn::Attribute>, derives: &[&str]) -> Result<()> {
2273 let derive_str = derives.join(", ");
2274
2275 let dummy = format!("#[derive({})]\nstruct Dummy;", derive_str);
2277 let parsed: syn::ItemStruct = parse_str(&dummy)
2278 .context("Failed to parse derive attribute")?;
2279
2280 let new_attr = parsed.attrs.into_iter()
2281 .find(|a| a.path().is_ident("derive"))
2282 .context("Failed to extract derive attribute")?;
2283
2284 if let Some(pos) = attrs.iter().position(|a| a.path().is_ident("derive")) {
2286 attrs[pos] = new_attr;
2287 } else {
2288 attrs.insert(0, new_attr);
2290 }
2291
2292 Ok(())
2293 }
2294
2295 fn replace_modified_functions(&mut self, modified_function: &Option<String>) -> Result<()> {
2297 if modified_function.is_none() {
2299 self.content = prettyplease::unparse(&self.syntax_tree);
2302 return Ok(());
2303 }
2304
2305 let original_syntax_tree: File = syn::parse_str(&self.content)
2307 .context("Failed to re-parse original content")?;
2308
2309 let function_name = modified_function.as_ref().unwrap();
2310
2311 let original_fn = original_syntax_tree.items.iter()
2313 .find_map(|item| {
2314 if let Item::Fn(f) = item {
2315 if f.sig.ident == function_name {
2316 return Some(f.clone());
2317 }
2318 }
2319 None
2320 })
2321 .ok_or_else(|| anyhow::anyhow!("Function '{}' not found in original", function_name))?;
2322
2323 let start = self.span_to_byte_offset(original_fn.span().start());
2325 let end = self.span_to_byte_offset(original_fn.span().end());
2326
2327 let modified_fn = self.syntax_tree.items.iter()
2329 .find_map(|item| {
2330 if let Item::Fn(f) = item {
2331 if f.sig.ident == function_name {
2332 return Some(f.clone());
2333 }
2334 }
2335 None
2336 })
2337 .ok_or_else(|| anyhow::anyhow!("Function '{}' not found in modified AST", function_name))?;
2338
2339 let dummy_file = syn::File {
2341 shebang: None,
2342 attrs: Vec::new(),
2343 items: vec![Item::Fn(modified_fn)],
2344 };
2345
2346 let formatted_fn = prettyplease::unparse(&dummy_file);
2347
2348 let formatted_fn = formatted_fn.trim();
2350
2351 self.content.replace_range(start..end, formatted_fn);
2353
2354 Ok(())
2355 }
2356
2357 pub fn span_to_byte_offset(&self, pos: LineColumn) -> usize {
2358 let line_idx = pos.line.saturating_sub(1);
2359 if line_idx < self.line_offsets.len() {
2360 self.line_offsets[line_idx] + pos.column
2361 } else {
2362 self.content.len()
2363 }
2364 }
2365
2366 fn find_after_field_end(&self, pos: usize) -> usize {
2367 let mut i = pos;
2369 while i < self.content.len() {
2370 match self.content.as_bytes()[i] as char {
2371 ',' => return i + 1,
2372 '\n' => return i + 1,
2373 _ => i += 1,
2374 }
2375 }
2376 pos
2377 }
2378
2379 fn get_indentation(&self, pos: usize) -> String {
2380 let mut line_start = pos;
2382 while line_start > 0 && self.content.as_bytes()[line_start - 1] != b'\n' {
2383 line_start -= 1;
2384 }
2385
2386 let mut indent = String::new();
2388 let mut i = line_start;
2389 while i < self.content.len() {
2390 match self.content.as_bytes()[i] as char {
2391 ' ' | '\t' => {
2392 indent.push(self.content.as_bytes()[i] as char);
2393 i += 1;
2394 }
2395 _ => break,
2396 }
2397 }
2398
2399 if indent.is_empty() {
2401 " ".to_string()
2402 } else {
2403 indent
2404 }
2405 }
2406
2407 pub fn to_string(&self) -> String {
2408 self.content.clone()
2409 }
2410
2411 pub fn get_syntax_tree(&self) -> &syn::File {
2413 &self.syntax_tree
2414 }
2415
2416 pub fn replace_range(&mut self, start: usize, end: usize, new_content: &str) -> Result<()> {
2418 if start > end || end > self.content.len() {
2419 anyhow::bail!("Invalid range: {}..{} (content length: {})", start, end, self.content.len());
2420 }
2421
2422 self.content.replace_range(start..end, new_content);
2423
2424 self.syntax_tree = syn::parse_str(&self.content)
2426 .context("Failed to re-parse after replace_range")?;
2427 self.line_offsets = Self::compute_line_offsets(&self.content);
2428
2429 Ok(())
2430 }
2431
2432 pub fn find_field_locations(&self, field_name: &str) -> Result<Vec<crate::operations::FieldLocation>> {
2434 use syn::visit::Visit;
2435 use crate::operations::{FieldLocation, FieldContext};
2436
2437 let mut locations = Vec::new();
2438
2439 for item in &self.syntax_tree.items {
2441 if let Item::Struct(s) = item {
2442 if let Fields::Named(ref fields) = s.fields {
2443 for field in &fields.named {
2444 if let Some(ident) = &field.ident {
2445 if ident == field_name {
2446 let field_type = quote::quote!(#field).to_string()
2447 .split(':')
2448 .nth(1)
2449 .map(|s| s.trim().to_string())
2450 .unwrap_or_else(|| "unknown".to_string());
2451 locations.push(FieldLocation {
2452 file_path: String::new(),
2453 line: s.span().start().line,
2454 context: FieldContext::StructDefinition {
2455 struct_name: s.ident.to_string(),
2456 field_type,
2457 },
2458 });
2459 }
2460 }
2461 }
2462 }
2463 }
2464
2465 if let Item::Enum(e) = item {
2467 for variant in &e.variants {
2468 if let Fields::Named(ref fields) = variant.fields {
2469 for field in &fields.named {
2470 if let Some(ident) = &field.ident {
2471 if ident == field_name {
2472 let field_type = quote::quote!(#field).to_string()
2473 .split(':')
2474 .nth(1)
2475 .map(|s| s.trim().to_string())
2476 .unwrap_or_else(|| "unknown".to_string());
2477 locations.push(FieldLocation {
2478 file_path: String::new(),
2479 line: variant.span().start().line,
2480 context: FieldContext::EnumVariantDefinition {
2481 enum_name: e.ident.to_string(),
2482 variant_name: variant.ident.to_string(),
2483 field_type,
2484 },
2485 });
2486 }
2487 }
2488 }
2489 }
2490 }
2491 }
2492 }
2493
2494 struct LiteralVisitor<'a> {
2496 field_name: &'a str,
2497 locations: Vec<FieldLocation>,
2498 }
2499
2500 impl<'ast, 'a> Visit<'ast> for LiteralVisitor<'a> {
2501 fn visit_expr(&mut self, node: &'ast Expr) {
2502 if let Expr::Struct(expr_struct) = node {
2503 for field_value in &expr_struct.fields {
2505 if let syn::Member::Named(ident) = &field_value.member {
2506 if ident == self.field_name {
2507 let struct_name = expr_struct.path.segments.iter()
2508 .map(|seg| seg.ident.to_string())
2509 .collect::<Vec<_>>()
2510 .join("::");
2511
2512 self.locations.push(FieldLocation {
2513 file_path: String::new(),
2514 line: expr_struct.span().start().line,
2515 context: FieldContext::StructLiteral {
2516 struct_name,
2517 },
2518 });
2519 break;
2520 }
2521 }
2522 }
2523 }
2524 syn::visit::visit_expr(self, node);
2525 }
2526 }
2527
2528 let mut visitor = LiteralVisitor {
2529 field_name,
2530 locations: Vec::new(),
2531 };
2532
2533 visitor.visit_file(&self.syntax_tree);
2534 locations.extend(visitor.locations);
2535
2536 Ok(locations)
2537 }
2538
2539 pub fn inspect(&self, node_type: Option<&str>, name_filter: Option<&str>, variant_filter: Option<&str>, include_comments: bool) -> Result<Vec<crate::operations::InspectResult>> {
2541 use syn::visit::Visit;
2542 use crate::operations::InspectResult;
2543
2544 let mut results = Vec::new();
2545
2546 if node_type.is_none() {
2548 let all_types = vec![
2549 "struct", "enum", "function", "impl-method", "trait", "trait-impl", "const", "static", "type-alias", "mod",
2550 "struct-literal", "match-arm", "enum-usage", "function-call", "method-call", "macro-call", "identifier", "type-ref",
2551 ];
2552 for nt in all_types {
2553 let mut type_results = self.inspect(Some(nt), name_filter, variant_filter, include_comments)?;
2554 results.append(&mut type_results);
2555 }
2556 return Ok(results);
2557 }
2558
2559 let node_type = node_type.unwrap(); match node_type {
2562 "struct-literal" => {
2563 struct StructLiteralVisitor<'a> {
2565 results: &'a mut Vec<InspectResult>,
2566 name_filter: Option<&'a str>,
2567 editor: &'a RustEditor,
2568 include_comments: bool,
2569 }
2570
2571 impl<'ast, 'a> Visit<'ast> for StructLiteralVisitor<'a> {
2572 fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
2573 use syn::parse::Parser;
2576
2577 if let Ok(exprs) = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated
2579 .parse2(node.mac.tokens.clone())
2580 {
2581 for expr in exprs.iter() {
2582 syn::visit::visit_expr(self, expr);
2583 }
2584 }
2585
2586 syn::visit::visit_expr_macro(self, node);
2588 }
2589
2590 fn visit_expr_struct(&mut self, node: &'ast syn::ExprStruct) {
2591 let filter = match self.name_filter {
2597 Some(f) => f,
2598 None => {
2599 let struct_name = if node.path.segments.len() > 1 {
2602 node.path.segments.iter()
2603 .map(|seg| seg.ident.to_string())
2604 .collect::<Vec<_>>()
2605 .join("::")
2606 } else {
2607 node.path.segments.last()
2608 .map(|seg| seg.ident.to_string())
2609 .unwrap_or_default()
2610 };
2611
2612 let snippet = self.editor.format_expr_struct(node);
2613 let location = self.editor.span_to_location(node.span());
2614
2615 let preceding_comment = if self.include_comments {
2617 extract_preceding_comment(&self.editor.content, location.line)
2618 } else {
2619 None
2620 };
2621
2622 let preceding_comment = if self.include_comments {
2624 extract_preceding_comment(&self.editor.content, location.line)
2625 } else {
2626 None
2627 };
2628
2629 self.results.push(InspectResult {
2630 file_path: String::new(),
2631 node_type: "struct-literal".to_string(),
2632 identifier: struct_name,
2633 location,
2634 snippet,
2635 preceding_comment,
2636 });
2637
2638 syn::visit::visit_expr_struct(self, node);
2639 return;
2640 }
2641 };
2642
2643 let matches = if filter.contains("::") {
2645 if filter.starts_with("*::") {
2647 let target_name = &filter[3..]; node.path.segments.last()
2650 .map(|seg| seg.ident.to_string() == target_name)
2651 .unwrap_or(false)
2652 } else {
2653 let path_str = node.path.segments.iter()
2655 .map(|seg| seg.ident.to_string())
2656 .collect::<Vec<_>>()
2657 .join("::");
2658 path_str == filter
2659 }
2660 } else {
2661 node.path.segments.last()
2664 .map(|seg| seg.ident.to_string() == filter)
2665 .unwrap_or(false)
2666 };
2667
2668 if !matches {
2669 syn::visit::visit_expr_struct(self, node);
2670 return;
2671 }
2672
2673 let struct_name = if node.path.segments.len() > 1 {
2676 node.path.segments.iter()
2677 .map(|seg| seg.ident.to_string())
2678 .collect::<Vec<_>>()
2679 .join("::")
2680 } else {
2681 node.path.segments.last()
2682 .map(|seg| seg.ident.to_string())
2683 .unwrap_or_default()
2684 };
2685
2686 let snippet = self.editor.format_expr_struct(node);
2688 let location = self.editor.span_to_location(node.span());
2689
2690 let preceding_comment = if self.include_comments {
2692 extract_preceding_comment(&self.editor.content, location.line)
2693 } else {
2694 None
2695 };
2696
2697 let preceding_comment = if self.include_comments {
2699 extract_preceding_comment(&self.editor.content, location.line)
2700 } else {
2701 None
2702 };
2703
2704 self.results.push(InspectResult {
2705 file_path: String::new(), node_type: "struct-literal".to_string(),
2707 identifier: struct_name,
2708 location,
2709 snippet,
2710 preceding_comment,
2711 });
2712
2713 syn::visit::visit_expr_struct(self, node);
2715 }
2716 }
2717
2718 let mut visitor = StructLiteralVisitor {
2719 results: &mut results,
2720 name_filter,
2721 editor: self,
2722 include_comments,
2723 };
2724
2725 for item in &self.syntax_tree.items {
2727 syn::visit::visit_item(&mut visitor, item);
2728 }
2729 }
2730 "match-arm" => {
2731 struct MatchArmVisitor<'a> {
2733 results: &'a mut Vec<InspectResult>,
2734 pattern_filter: Option<&'a str>,
2735 editor: &'a RustEditor,
2736 include_comments: bool,
2737 }
2738
2739 impl<'ast, 'a> Visit<'ast> for MatchArmVisitor<'a> {
2740 fn visit_expr_match(&mut self, node: &'ast syn::ExprMatch) {
2741 for arm in &node.arms {
2743 let pat = &arm.pat;
2745 let pattern_str = quote::quote!(#pat).to_string();
2746
2747 if let Some(filter) = self.pattern_filter {
2749 let normalized_pattern = pattern_str.replace(" ", "");
2751 let normalized_filter = filter.replace(" ", "");
2752
2753 if !normalized_pattern.contains(&normalized_filter) {
2754 continue;
2755 }
2756 }
2757
2758 let snippet = self.editor.format_match_arm(arm);
2760 let location = self.editor.span_to_location(arm.span());
2761
2762 let preceding_comment = if self.include_comments {
2764 extract_preceding_comment(&self.editor.content, location.line)
2765 } else {
2766 None
2767 };
2768
2769 let preceding_comment = if self.include_comments {
2771 extract_preceding_comment(&self.editor.content, location.line)
2772 } else {
2773 None
2774 };
2775
2776 self.results.push(InspectResult {
2777 file_path: String::new(), node_type: "match-arm".to_string(),
2779 identifier: pattern_str.replace(" ", ""),
2780 location,
2781 snippet,
2782 preceding_comment,
2783 });
2784 }
2785
2786 syn::visit::visit_expr_match(self, node);
2788 }
2789 }
2790
2791 let mut visitor = MatchArmVisitor {
2792 results: &mut results,
2793 pattern_filter: name_filter,
2794 editor: self,
2795 include_comments,
2796 };
2797
2798 for item in &self.syntax_tree.items {
2800 syn::visit::visit_item(&mut visitor, item);
2801 }
2802 }
2803 "enum-usage" => {
2804 struct EnumUsageVisitor<'a> {
2806 results: &'a mut Vec<InspectResult>,
2807 path_filter: Option<&'a str>,
2808 editor: &'a RustEditor,
2809 include_comments: bool,
2810 }
2811
2812 impl<'ast, 'a> Visit<'ast> for EnumUsageVisitor<'a> {
2813 fn visit_expr_path(&mut self, node: &'ast syn::ExprPath) {
2814 let path = &node.path;
2816 let path_str = quote::quote!(#path).to_string();
2817
2818 if let Some(filter) = self.path_filter {
2820 let normalized_path = path_str.replace(" ", "");
2822 let normalized_filter = filter.replace(" ", "");
2823
2824 if !normalized_path.contains(&normalized_filter) {
2825 syn::visit::visit_expr_path(self, node);
2826 return;
2827 }
2828 }
2829
2830 let snippet = self.editor.format_expr_path(node);
2832 let location = self.editor.span_to_location(node.span());
2833
2834 let preceding_comment = if self.include_comments {
2836 extract_preceding_comment(&self.editor.content, location.line)
2837 } else {
2838 None
2839 };
2840
2841 let preceding_comment = if self.include_comments {
2843 extract_preceding_comment(&self.editor.content, location.line)
2844 } else {
2845 None
2846 };
2847
2848 self.results.push(InspectResult {
2849 file_path: String::new(), node_type: "enum-usage".to_string(),
2851 identifier: path_str.replace(" ", ""),
2852 location,
2853 snippet,
2854 preceding_comment,
2855 });
2856
2857 syn::visit::visit_expr_path(self, node);
2859 }
2860 }
2861
2862 let mut visitor = EnumUsageVisitor {
2863 results: &mut results,
2864 path_filter: name_filter,
2865 editor: self,
2866 include_comments,
2867 };
2868
2869 for item in &self.syntax_tree.items {
2871 syn::visit::visit_item(&mut visitor, item);
2872 }
2873 }
2874 "function-call" => {
2875 struct FunctionCallVisitor<'a> {
2877 results: &'a mut Vec<InspectResult>,
2878 name_filter: Option<&'a str>,
2879 editor: &'a RustEditor,
2880 include_comments: bool,
2881 }
2882
2883 impl<'ast, 'a> Visit<'ast> for FunctionCallVisitor<'a> {
2884 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
2885 let func_name = if let syn::Expr::Path(expr_path) = &*node.func {
2887 expr_path.path.segments.last()
2889 .map(|seg| seg.ident.to_string())
2890 .unwrap_or_default()
2891 } else {
2892 quote::quote!(#node.func).to_string()
2894 };
2895
2896 if let Some(filter) = self.name_filter {
2898 if func_name != filter {
2899 syn::visit::visit_expr_call(self, node);
2900 return;
2901 }
2902 }
2903
2904 let snippet = self.editor.format_expr_call(node);
2906 let location = self.editor.span_to_location(node.span());
2907
2908 let preceding_comment = if self.include_comments {
2910 extract_preceding_comment(&self.editor.content, location.line)
2911 } else {
2912 None
2913 };
2914
2915 let preceding_comment = if self.include_comments {
2917 extract_preceding_comment(&self.editor.content, location.line)
2918 } else {
2919 None
2920 };
2921
2922 self.results.push(InspectResult {
2923 file_path: String::new(), node_type: "function-call".to_string(),
2925 identifier: func_name,
2926 location,
2927 snippet,
2928 preceding_comment,
2929 });
2930
2931 syn::visit::visit_expr_call(self, node);
2933 }
2934 }
2935
2936 let mut visitor = FunctionCallVisitor {
2937 results: &mut results,
2938 name_filter,
2939 editor: self,
2940 include_comments,
2941 };
2942
2943 for item in &self.syntax_tree.items {
2945 syn::visit::visit_item(&mut visitor, item);
2946 }
2947 }
2948 "trait-method" => {
2949 struct TraitMethodVisitor<'a> {
2951 results: &'a mut Vec<InspectResult>,
2952 name_filter: Option<&'a str>,
2953 editor: &'a RustEditor,
2954 include_comments: bool,
2955 current_trait_name: Option<String>,
2956 }
2957
2958 impl<'ast, 'a> Visit<'ast> for TraitMethodVisitor<'a> {
2959 fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
2960 let prev_trait_name = self.current_trait_name.clone();
2962 self.current_trait_name = Some(node.ident.to_string());
2963
2964 syn::visit::visit_item_trait(self, node);
2965
2966 self.current_trait_name = prev_trait_name;
2967 }
2968
2969 fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
2970 let method_name = node.sig.ident.to_string();
2971
2972 let identifier = if let Some(ref trait_name) = self.current_trait_name {
2974 format!("{}::{}", trait_name, method_name)
2975 } else {
2976 method_name.clone()
2977 };
2978
2979 if let Some(filter) = self.name_filter {
2981 if !identifier.contains(filter) && method_name != filter {
2983 syn::visit::visit_trait_item_fn(self, node);
2984 return;
2985 }
2986 }
2987
2988 let snippet = self.editor.format_trait_item_fn(node);
2990 let location = self.editor.span_to_location(node.span());
2991
2992 let preceding_comment = if self.include_comments {
2994 extract_preceding_comment(&self.editor.content, location.line)
2995 } else {
2996 None
2997 };
2998
2999 self.results.push(InspectResult {
3000 file_path: String::new(),
3001 node_type: "trait-method".to_string(),
3002 identifier,
3003 location,
3004 snippet,
3005 preceding_comment,
3006 });
3007
3008 syn::visit::visit_trait_item_fn(self, node);
3009 }
3010 }
3011
3012 let mut visitor = TraitMethodVisitor {
3013 results: &mut results,
3014 name_filter,
3015 editor: self,
3016 include_comments,
3017 current_trait_name: None,
3018 };
3019
3020 for item in &self.syntax_tree.items {
3021 syn::visit::visit_item(&mut visitor, item);
3022 }
3023 }
3024 "method-call" => {
3025 struct MethodCallVisitor<'a> {
3027 results: &'a mut Vec<InspectResult>,
3028 name_filter: Option<&'a str>,
3029 editor: &'a RustEditor,
3030 include_comments: bool,
3031 }
3032
3033 impl<'ast, 'a> Visit<'ast> for MethodCallVisitor<'a> {
3034 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
3035 let method_name = node.method.to_string();
3037
3038 if let Some(filter) = self.name_filter {
3040 if method_name != filter {
3041 syn::visit::visit_expr_method_call(self, node);
3042 return;
3043 }
3044 }
3045
3046 let snippet = self.editor.format_expr_method_call(node);
3048 let location = self.editor.span_to_location(node.span());
3049
3050 let preceding_comment = if self.include_comments {
3052 extract_preceding_comment(&self.editor.content, location.line)
3053 } else {
3054 None
3055 };
3056
3057 self.results.push(InspectResult {
3058 file_path: String::new(), node_type: "method-call".to_string(),
3060 identifier: method_name,
3061 location,
3062 snippet,
3063 preceding_comment,
3064 });
3065
3066 syn::visit::visit_expr_method_call(self, node);
3068 }
3069 }
3070
3071 let mut visitor = MethodCallVisitor {
3072 results: &mut results,
3073 name_filter,
3074 editor: self,
3075 include_comments,
3076 };
3077
3078 for item in &self.syntax_tree.items {
3080 syn::visit::visit_item(&mut visitor, item);
3081 }
3082 }
3083 "identifier" => {
3084 struct IdentifierVisitor<'a> {
3086 results: &'a mut Vec<InspectResult>,
3087 name_filter: Option<&'a str>,
3088 editor: &'a RustEditor,
3089 include_comments: bool,
3090 }
3091
3092 impl<'ast, 'a> Visit<'ast> for IdentifierVisitor<'a> {
3093 fn visit_ident(&mut self, node: &'ast syn::Ident) {
3094 let ident_name = node.to_string();
3096
3097 if let Some(filter) = self.name_filter {
3099 if ident_name != filter {
3100 syn::visit::visit_ident(self, node);
3101 return;
3102 }
3103 }
3104
3105 let snippet = self.editor.format_ident(node);
3107 let location = self.editor.span_to_location(node.span());
3108
3109 let preceding_comment = if self.include_comments {
3111 extract_preceding_comment(&self.editor.content, location.line)
3112 } else {
3113 None
3114 };
3115
3116 self.results.push(InspectResult {
3117 file_path: String::new(), node_type: "identifier".to_string(),
3119 identifier: ident_name,
3120 location,
3121 snippet,
3122 preceding_comment,
3123 });
3124
3125 syn::visit::visit_ident(self, node);
3127 }
3128 }
3129
3130 let mut visitor = IdentifierVisitor {
3131 results: &mut results,
3132 name_filter,
3133 editor: self,
3134 include_comments,
3135 };
3136
3137 for item in &self.syntax_tree.items {
3139 syn::visit::visit_item(&mut visitor, item);
3140 }
3141 }
3142 "type-ref" => {
3143 struct TypeRefVisitor<'a> {
3145 results: &'a mut Vec<InspectResult>,
3146 name_filter: Option<&'a str>,
3147 editor: &'a RustEditor,
3148 include_comments: bool,
3149 }
3150
3151 impl<'ast, 'a> Visit<'ast> for TypeRefVisitor<'a> {
3152 fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
3153 let type_name = node.path.segments.last()
3155 .map(|seg| seg.ident.to_string())
3156 .unwrap_or_default();
3157
3158 if let Some(filter) = self.name_filter {
3160 if type_name != filter {
3161 syn::visit::visit_type_path(self, node);
3162 return;
3163 }
3164 }
3165
3166 let snippet = self.editor.format_type_path(node);
3168 let location = self.editor.span_to_location(node.span());
3169
3170 let preceding_comment = if self.include_comments {
3172 extract_preceding_comment(&self.editor.content, location.line)
3173 } else {
3174 None
3175 };
3176
3177 let path = &node.path;
3179 let path_str = quote::quote!(#path).to_string();
3180
3181 self.results.push(InspectResult {
3182 file_path: String::new(), node_type: "type-ref".to_string(),
3184 identifier: path_str.replace(" ", ""),
3185 location,
3186 snippet,
3187 preceding_comment,
3188 });
3189
3190 syn::visit::visit_type_path(self, node);
3192 }
3193 }
3194
3195 let mut visitor = TypeRefVisitor {
3196 results: &mut results,
3197 name_filter,
3198 editor: self,
3199 include_comments,
3200 };
3201
3202 for item in &self.syntax_tree.items {
3204 syn::visit::visit_item(&mut visitor, item);
3205 }
3206 }
3207 "macro-call" => {
3208 struct MacroCallVisitor<'a> {
3210 results: &'a mut Vec<InspectResult>,
3211 name_filter: Option<&'a str>,
3212 editor: &'a RustEditor,
3213 include_comments: bool,
3214 }
3215
3216 impl<'ast, 'a> Visit<'ast> for MacroCallVisitor<'a> {
3217 fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
3218 let macro_name = node.mac.path.segments.last()
3220 .map(|seg| seg.ident.to_string())
3221 .unwrap_or_default();
3222
3223 if let Some(filter) = self.name_filter {
3225 if macro_name != filter {
3226 syn::visit::visit_expr_macro(self, node);
3227 return;
3228 }
3229 }
3230
3231 let snippet = self.editor.format_expr_macro(node);
3233 let location = self.editor.span_to_location(node.span());
3234
3235 let preceding_comment = if self.include_comments {
3237 extract_preceding_comment(&self.editor.content, location.line)
3238 } else {
3239 None
3240 };
3241
3242 self.results.push(InspectResult {
3243 file_path: String::new(), node_type: "macro-call".to_string(),
3245 identifier: macro_name,
3246 location,
3247 snippet,
3248 preceding_comment,
3249 });
3250
3251 syn::visit::visit_expr_macro(self, node);
3253 }
3254
3255 fn visit_stmt(&mut self, node: &'ast syn::Stmt) {
3256 if let syn::Stmt::Macro(macro_stmt) = node {
3258 let macro_name = macro_stmt.mac.path.segments.last()
3259 .map(|seg| seg.ident.to_string())
3260 .unwrap_or_default();
3261
3262 if let Some(filter) = self.name_filter {
3264 if macro_name != filter {
3265 syn::visit::visit_stmt(self, node);
3266 return;
3267 }
3268 }
3269
3270 let snippet = self.editor.format_stmt_macro(macro_stmt);
3272 let location = self.editor.span_to_location(macro_stmt.span());
3273
3274 let preceding_comment = if self.include_comments {
3276 extract_preceding_comment(&self.editor.content, location.line)
3277 } else {
3278 None
3279 };
3280
3281 self.results.push(InspectResult {
3282 file_path: String::new(), node_type: "macro-call".to_string(),
3284 identifier: macro_name,
3285 location,
3286 snippet,
3287 preceding_comment,
3288 });
3289 }
3290
3291 syn::visit::visit_stmt(self, node);
3293 }
3294 }
3295
3296 let mut visitor = MacroCallVisitor {
3297 results: &mut results,
3298 name_filter,
3299 editor: self,
3300 include_comments,
3301 };
3302
3303 for item in &self.syntax_tree.items {
3305 syn::visit::visit_item(&mut visitor, item);
3306 }
3307 }
3308 "struct" => {
3309 struct StructDefVisitor<'a> {
3311 results: &'a mut Vec<InspectResult>,
3312 name_filter: Option<&'a str>,
3313 editor: &'a RustEditor,
3314 include_comments: bool,
3315 }
3316
3317 impl<'ast, 'a> Visit<'ast> for StructDefVisitor<'a> {
3318 fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
3319 let struct_name = node.ident.to_string();
3320
3321 if let Some(filter) = self.name_filter {
3323 if struct_name != filter {
3324 syn::visit::visit_item_struct(self, node);
3325 return;
3326 }
3327 }
3328
3329 let snippet = self.editor.format_item_struct(node);
3331 let location = self.editor.span_to_location(node.span());
3332
3333 let preceding_comment = if self.include_comments {
3335 extract_preceding_comment(&self.editor.content, location.line)
3336 } else {
3337 None
3338 };
3339
3340 self.results.push(InspectResult {
3341 file_path: String::new(),
3342 node_type: "struct".to_string(),
3343 identifier: struct_name,
3344 location,
3345 snippet,
3346 preceding_comment,
3347 });
3348
3349 syn::visit::visit_item_struct(self, node);
3350 }
3351 }
3352
3353 let mut visitor = StructDefVisitor {
3354 results: &mut results,
3355 name_filter,
3356 editor: self,
3357 include_comments,
3358 };
3359
3360 for item in &self.syntax_tree.items {
3361 syn::visit::visit_item(&mut visitor, item);
3362 }
3363 }
3364 "enum" => {
3365 struct EnumDefVisitor<'a> {
3372 results: &'a mut Vec<InspectResult>,
3373 name_filter: Option<&'a str>,
3374 variant_filter: Option<&'a str>,
3375 editor: &'a RustEditor,
3376 include_comments: bool,
3377 }
3378
3379 impl<'ast, 'a> Visit<'ast> for EnumDefVisitor<'a> {
3380 fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
3381 let enum_name = node.ident.to_string();
3382
3383 let (enum_name_filter, implicit_variant_filter) = if let Some(filter) = self.name_filter {
3385 if filter.contains("::") {
3386 let parts: Vec<&str> = filter.split("::").collect();
3388 if parts.len() == 2 {
3389 if parts[0] == "*" {
3390 (None, Some(parts[1]))
3392 } else {
3393 (Some(parts[0]), Some(parts[1]))
3395 }
3396 } else {
3397 (Some(filter), None)
3399 }
3400 } else {
3401 (Some(filter), None)
3403 }
3404 } else {
3405 (None, None)
3406 };
3407
3408 let effective_variant_filter = self.variant_filter.or(implicit_variant_filter);
3410
3411 if let Some(filter) = enum_name_filter {
3413 if enum_name != filter {
3414 syn::visit::visit_item_enum(self, node);
3415 return;
3416 }
3417 }
3418
3419 if let Some(variant_name) = effective_variant_filter {
3421 let has_variant = node.variants.iter().any(|v| v.ident.to_string() == variant_name);
3422 if !has_variant {
3423 syn::visit::visit_item_enum(self, node);
3424 return;
3425 }
3426 }
3427
3428 let snippet = if let Some(variant_name) = effective_variant_filter {
3430 self.editor.format_item_enum_variant_only(node, variant_name)
3432 } else {
3433 self.editor.format_item_enum(node)
3434 };
3435
3436 let location = self.editor.span_to_location(node.span());
3437
3438 let preceding_comment = if self.include_comments {
3440 extract_preceding_comment(&self.editor.content, location.line)
3441 } else {
3442 None
3443 };
3444
3445 self.results.push(InspectResult {
3446 file_path: String::new(),
3447 node_type: "enum".to_string(),
3448 identifier: enum_name,
3449 location,
3450 snippet,
3451 preceding_comment,
3452 });
3453
3454 syn::visit::visit_item_enum(self, node);
3455 }
3456 }
3457
3458 let mut visitor = EnumDefVisitor {
3459 results: &mut results,
3460 name_filter,
3461 variant_filter,
3462 editor: self,
3463 include_comments,
3464 };
3465
3466 for item in &self.syntax_tree.items {
3467 syn::visit::visit_item(&mut visitor, item);
3468 }
3469 }
3470 "function" => {
3471 struct FunctionDefVisitor<'a> {
3473 results: &'a mut Vec<InspectResult>,
3474 name_filter: Option<&'a str>,
3475 editor: &'a RustEditor,
3476 include_comments: bool,
3477 }
3478
3479 impl<'ast, 'a> Visit<'ast> for FunctionDefVisitor<'a> {
3480 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
3481 let fn_name = node.sig.ident.to_string();
3482
3483 if let Some(filter) = self.name_filter {
3485 if fn_name != filter {
3486 syn::visit::visit_item_fn(self, node);
3487 return;
3488 }
3489 }
3490
3491 let snippet = self.editor.format_item_fn(node);
3493 let location = self.editor.span_to_location(node.span());
3494
3495 let preceding_comment = if self.include_comments {
3497 extract_preceding_comment(&self.editor.content, location.line)
3498 } else {
3499 None
3500 };
3501
3502 self.results.push(InspectResult {
3503 file_path: String::new(),
3504 node_type: "function".to_string(),
3505 identifier: fn_name,
3506 location,
3507 snippet,
3508 preceding_comment,
3509 });
3510
3511 syn::visit::visit_item_fn(self, node);
3512 }
3513 }
3514
3515 let mut visitor = FunctionDefVisitor {
3516 results: &mut results,
3517 name_filter,
3518 editor: self,
3519 include_comments,
3520 };
3521
3522 for item in &self.syntax_tree.items {
3523 syn::visit::visit_item(&mut visitor, item);
3524 }
3525 }
3526 "impl-method" => {
3527 struct ImplMethodVisitor<'a> {
3529 results: &'a mut Vec<InspectResult>,
3530 name_filter: Option<&'a str>,
3531 editor: &'a RustEditor,
3532 include_comments: bool,
3533 current_impl_type: Option<String>,
3534 }
3535
3536 impl<'ast, 'a> Visit<'ast> for ImplMethodVisitor<'a> {
3537 fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
3538 let impl_type = if let syn::Type::Path(type_path) = &*node.self_ty {
3540 type_path.path.segments.last()
3541 .map(|seg| seg.ident.to_string())
3542 } else {
3543 None
3544 };
3545
3546 let prev_impl_type = self.current_impl_type.clone();
3547 self.current_impl_type = impl_type;
3548
3549 syn::visit::visit_item_impl(self, node);
3550
3551 self.current_impl_type = prev_impl_type;
3552 }
3553
3554 fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
3555 let method_name = node.sig.ident.to_string();
3556
3557 let identifier = if let Some(ref impl_type) = self.current_impl_type {
3559 format!("{}::{}", impl_type, method_name)
3560 } else {
3561 method_name.clone()
3562 };
3563
3564 if let Some(filter) = self.name_filter {
3566 if !identifier.contains(filter) && method_name != filter {
3568 syn::visit::visit_impl_item_fn(self, node);
3569 return;
3570 }
3571 }
3572
3573 let snippet = self.editor.format_impl_item_fn(node);
3575 let location = self.editor.span_to_location(node.span());
3576
3577 let preceding_comment = if self.include_comments {
3579 extract_preceding_comment(&self.editor.content, location.line)
3580 } else {
3581 None
3582 };
3583
3584 self.results.push(InspectResult {
3585 file_path: String::new(),
3586 node_type: "impl-method".to_string(),
3587 identifier,
3588 location,
3589 snippet,
3590 preceding_comment,
3591 });
3592
3593 syn::visit::visit_impl_item_fn(self, node);
3594 }
3595 }
3596
3597 let mut visitor = ImplMethodVisitor {
3598 results: &mut results,
3599 name_filter,
3600 editor: self,
3601 include_comments,
3602 current_impl_type: None,
3603 };
3604
3605 for item in &self.syntax_tree.items {
3606 syn::visit::visit_item(&mut visitor, item);
3607 }
3608 }
3609 "trait" => {
3610 struct TraitDefVisitor<'a> {
3612 results: &'a mut Vec<InspectResult>,
3613 name_filter: Option<&'a str>,
3614 editor: &'a RustEditor,
3615 include_comments: bool,
3616 }
3617
3618 impl<'ast, 'a> Visit<'ast> for TraitDefVisitor<'a> {
3619 fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
3620 let trait_name = node.ident.to_string();
3621
3622 if let Some(filter) = self.name_filter {
3624 if trait_name != filter {
3625 syn::visit::visit_item_trait(self, node);
3626 return;
3627 }
3628 }
3629
3630 let snippet = self.editor.format_item_trait(node);
3632 let location = self.editor.span_to_location(node.span());
3633
3634 let preceding_comment = if self.include_comments {
3636 extract_preceding_comment(&self.editor.content, location.line)
3637 } else {
3638 None
3639 };
3640
3641 self.results.push(InspectResult {
3642 file_path: String::new(),
3643 node_type: "trait".to_string(),
3644 identifier: trait_name,
3645 location,
3646 snippet,
3647 preceding_comment,
3648 });
3649
3650 syn::visit::visit_item_trait(self, node);
3651 }
3652 }
3653
3654 let mut visitor = TraitDefVisitor {
3655 results: &mut results,
3656 name_filter,
3657 editor: self,
3658 include_comments,
3659 };
3660
3661 for item in &self.syntax_tree.items {
3662 syn::visit::visit_item(&mut visitor, item);
3663 }
3664 }
3665 "const" => {
3666 struct ConstDefVisitor<'a> {
3668 results: &'a mut Vec<InspectResult>,
3669 name_filter: Option<&'a str>,
3670 editor: &'a RustEditor,
3671 include_comments: bool,
3672 }
3673
3674 impl<'ast, 'a> Visit<'ast> for ConstDefVisitor<'a> {
3675 fn visit_item_const(&mut self, node: &'ast syn::ItemConst) {
3676 let const_name = node.ident.to_string();
3677
3678 if let Some(filter) = self.name_filter {
3680 if const_name != filter {
3681 syn::visit::visit_item_const(self, node);
3682 return;
3683 }
3684 }
3685
3686 let snippet = self.editor.format_item_const(node);
3688 let location = self.editor.span_to_location(node.span());
3689
3690 let preceding_comment = if self.include_comments {
3692 extract_preceding_comment(&self.editor.content, location.line)
3693 } else {
3694 None
3695 };
3696
3697 self.results.push(InspectResult {
3698 file_path: String::new(),
3699 node_type: "const".to_string(),
3700 identifier: const_name,
3701 location,
3702 snippet,
3703 preceding_comment,
3704 });
3705
3706 syn::visit::visit_item_const(self, node);
3707 }
3708 }
3709
3710 let mut visitor = ConstDefVisitor {
3711 results: &mut results,
3712 name_filter,
3713 editor: self,
3714 include_comments,
3715 };
3716
3717 for item in &self.syntax_tree.items {
3718 syn::visit::visit_item(&mut visitor, item);
3719 }
3720 }
3721 "static" => {
3722 struct StaticDefVisitor<'a> {
3724 results: &'a mut Vec<InspectResult>,
3725 name_filter: Option<&'a str>,
3726 editor: &'a RustEditor,
3727 include_comments: bool,
3728 }
3729
3730 impl<'ast, 'a> Visit<'ast> for StaticDefVisitor<'a> {
3731 fn visit_item_static(&mut self, node: &'ast syn::ItemStatic) {
3732 let static_name = node.ident.to_string();
3733
3734 if let Some(filter) = self.name_filter {
3736 if static_name != filter {
3737 syn::visit::visit_item_static(self, node);
3738 return;
3739 }
3740 }
3741
3742 let snippet = self.editor.format_item_static(node);
3744 let location = self.editor.span_to_location(node.span());
3745
3746 let preceding_comment = if self.include_comments {
3748 extract_preceding_comment(&self.editor.content, location.line)
3749 } else {
3750 None
3751 };
3752
3753 self.results.push(InspectResult {
3754 file_path: String::new(),
3755 node_type: "static".to_string(),
3756 identifier: static_name,
3757 location,
3758 snippet,
3759 preceding_comment,
3760 });
3761
3762 syn::visit::visit_item_static(self, node);
3763 }
3764 }
3765
3766 let mut visitor = StaticDefVisitor {
3767 results: &mut results,
3768 name_filter,
3769 editor: self,
3770 include_comments,
3771 };
3772
3773 for item in &self.syntax_tree.items {
3774 syn::visit::visit_item(&mut visitor, item);
3775 }
3776 }
3777 "type-alias" => {
3778 struct TypeAliasVisitor<'a> {
3780 results: &'a mut Vec<InspectResult>,
3781 name_filter: Option<&'a str>,
3782 editor: &'a RustEditor,
3783 include_comments: bool,
3784 }
3785
3786 impl<'ast, 'a> Visit<'ast> for TypeAliasVisitor<'a> {
3787 fn visit_item_type(&mut self, node: &'ast syn::ItemType) {
3788 let type_name = node.ident.to_string();
3789
3790 if let Some(filter) = self.name_filter {
3792 if type_name != filter {
3793 syn::visit::visit_item_type(self, node);
3794 return;
3795 }
3796 }
3797
3798 let snippet = self.editor.format_item_type(node);
3800 let location = self.editor.span_to_location(node.span());
3801
3802 let preceding_comment = if self.include_comments {
3804 extract_preceding_comment(&self.editor.content, location.line)
3805 } else {
3806 None
3807 };
3808
3809 self.results.push(InspectResult {
3810 file_path: String::new(),
3811 node_type: "type-alias".to_string(),
3812 identifier: type_name,
3813 location,
3814 snippet,
3815 preceding_comment,
3816 });
3817
3818 syn::visit::visit_item_type(self, node);
3819 }
3820 }
3821
3822 let mut visitor = TypeAliasVisitor {
3823 results: &mut results,
3824 name_filter,
3825 editor: self,
3826 include_comments,
3827 };
3828
3829 for item in &self.syntax_tree.items {
3830 syn::visit::visit_item(&mut visitor, item);
3831 }
3832 }
3833 "mod" => {
3834 struct ModDefVisitor<'a> {
3836 results: &'a mut Vec<InspectResult>,
3837 name_filter: Option<&'a str>,
3838 editor: &'a RustEditor,
3839 include_comments: bool,
3840 }
3841
3842 impl<'ast, 'a> Visit<'ast> for ModDefVisitor<'a> {
3843 fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
3844 let mod_name = node.ident.to_string();
3845
3846 if let Some(filter) = self.name_filter {
3848 if mod_name != filter {
3849 syn::visit::visit_item_mod(self, node);
3850 return;
3851 }
3852 }
3853
3854 let snippet = self.editor.format_item_mod(node);
3856 let location = self.editor.span_to_location(node.span());
3857
3858 let preceding_comment = if self.include_comments {
3860 extract_preceding_comment(&self.editor.content, location.line)
3861 } else {
3862 None
3863 };
3864
3865 self.results.push(InspectResult {
3866 file_path: String::new(),
3867 node_type: "mod".to_string(),
3868 identifier: mod_name,
3869 location,
3870 snippet,
3871 preceding_comment,
3872 });
3873
3874 syn::visit::visit_item_mod(self, node);
3875 }
3876 }
3877
3878 let mut visitor = ModDefVisitor {
3879 results: &mut results,
3880 name_filter,
3881 editor: self,
3882 include_comments,
3883 };
3884
3885 for item in &self.syntax_tree.items {
3886 syn::visit::visit_item(&mut visitor, item);
3887 }
3888 }
3889 "trait-impl" => {
3890 struct TraitImplVisitor<'a> {
3892 results: &'a mut Vec<InspectResult>,
3893 name_filter: Option<&'a str>,
3894 editor: &'a RustEditor,
3895 }
3896
3897 impl<'ast, 'a> Visit<'ast> for TraitImplVisitor<'a> {
3898 fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
3899 if node.trait_.is_none() {
3901 syn::visit::visit_item_impl(self, node);
3902 return;
3903 }
3904
3905 let trait_path = &node.trait_.as_ref().unwrap().1;
3906 let trait_name = trait_path.segments.last()
3907 .map(|seg| seg.ident.to_string())
3908 .unwrap_or_default();
3909
3910 if let Some(filter) = self.name_filter {
3912 let matches = if filter.contains("::") {
3913 if filter.starts_with("*::") {
3914 let target = &filter[3..];
3915 trait_path.segments.last()
3916 .map(|seg| seg.ident.to_string() == target)
3917 .unwrap_or(false)
3918 } else {
3919 let path_str = trait_path.segments.iter()
3920 .map(|seg| seg.ident.to_string())
3921 .collect::<Vec<_>>()
3922 .join("::");
3923 path_str == filter
3924 }
3925 } else {
3926 trait_path.segments.last()
3927 .map(|seg| seg.ident.to_string() == filter)
3928 .unwrap_or(false)
3929 };
3930 if !matches {
3931 syn::visit::visit_item_impl(self, node);
3932 return;
3933 }
3934 }
3935
3936 let type_name = match &*node.self_ty {
3938 syn::Type::Path(tp) => {
3939 tp.path.segments.last()
3940 .map(|seg| seg.ident.to_string())
3941 .unwrap_or_else(|| quote::quote!(#tp).to_string())
3942 }
3943 other => quote::quote!(#other).to_string(),
3944 };
3945
3946 let identifier = format!("{} for {}", trait_name, type_name);
3947 let snippet = format!("impl {} for {} {{ ... }}", trait_name, type_name);
3948 let location = self.editor.span_to_location(node.impl_token.span);
3949
3950 self.results.push(InspectResult {
3951 file_path: String::new(),
3952 node_type: "trait-impl".to_string(),
3953 identifier,
3954 location,
3955 snippet,
3956 preceding_comment: None,
3957 });
3958
3959 syn::visit::visit_item_impl(self, node);
3960 }
3961 }
3962
3963 let mut visitor = TraitImplVisitor {
3964 results: &mut results,
3965 name_filter,
3966 editor: self,
3967 };
3968
3969 for item in &self.syntax_tree.items {
3970 syn::visit::visit_item(&mut visitor, item);
3971 }
3972 }
3973 _ => anyhow::bail!("Unsupported node type: {}", node_type),
3974 }
3975
3976 Ok(results)
3977 }
3978
3979 fn format_expr_struct(&self, expr: &syn::ExprStruct) -> String {
3981 let start = self.span_to_byte_offset(expr.span().start());
3983 let end = self.span_to_byte_offset(expr.span().end());
3984
3985 let original = &self.content[start..end];
3987
3988 original.split_whitespace().collect::<Vec<_>>().join(" ")
3990 }
3991
3992 fn format_match_arm(&self, arm: &syn::Arm) -> String {
3994 let start = self.span_to_byte_offset(arm.span().start());
3996 let end = self.span_to_byte_offset(arm.span().end());
3997
3998 let original = &self.content[start..end];
4000
4001 original.split_whitespace().collect::<Vec<_>>().join(" ")
4003 }
4004
4005 fn format_expr_path(&self, expr: &syn::ExprPath) -> String {
4007 let start = self.span_to_byte_offset(expr.span().start());
4009 let end = self.span_to_byte_offset(expr.span().end());
4010
4011 let original = &self.content[start..end];
4013
4014 original.split_whitespace().collect::<Vec<_>>().join(" ")
4016 }
4017
4018 fn format_expr_call(&self, expr: &syn::ExprCall) -> String {
4020 let start = self.span_to_byte_offset(expr.span().start());
4022 let end = self.span_to_byte_offset(expr.span().end());
4023
4024 let original = &self.content[start..end];
4026
4027 original.split_whitespace().collect::<Vec<_>>().join(" ")
4029 }
4030
4031 fn format_expr_method_call(&self, expr: &syn::ExprMethodCall) -> String {
4033 let start = self.span_to_byte_offset(expr.span().start());
4035 let end = self.span_to_byte_offset(expr.span().end());
4036
4037 let original = &self.content[start..end];
4039
4040 original.split_whitespace().collect::<Vec<_>>().join(" ")
4042 }
4043
4044 fn format_ident(&self, ident: &syn::Ident) -> String {
4046 ident.to_string()
4047 }
4048
4049 fn format_type_path(&self, ty: &syn::TypePath) -> String {
4051 let start = self.span_to_byte_offset(ty.span().start());
4053 let end = self.span_to_byte_offset(ty.span().end());
4054
4055 let original = &self.content[start..end];
4057
4058 original.split_whitespace().collect::<Vec<_>>().join(" ")
4060 }
4061
4062 fn format_expr_macro(&self, expr: &syn::ExprMacro) -> String {
4064 let start = self.span_to_byte_offset(expr.span().start());
4066 let end = self.span_to_byte_offset(expr.span().end());
4067
4068 let original = &self.content[start..end];
4070
4071 original.split_whitespace().collect::<Vec<_>>().join(" ")
4073 }
4074
4075 fn format_stmt_macro(&self, stmt: &syn::StmtMacro) -> String {
4077 let start = self.span_to_byte_offset(stmt.span().start());
4079 let end = self.span_to_byte_offset(stmt.span().end());
4080
4081 let original = &self.content[start..end];
4083
4084 original.split_whitespace().collect::<Vec<_>>().join(" ")
4086 }
4087
4088 fn format_item_struct(&self, item: &syn::ItemStruct) -> String {
4090 let start = self.span_to_byte_offset(item.span().start());
4091 let end = self.span_to_byte_offset(item.span().end());
4092 let original = &self.content[start..end];
4093 original.to_string()
4094 }
4095
4096 fn format_item_enum(&self, item: &syn::ItemEnum) -> String {
4098 let start = self.span_to_byte_offset(item.span().start());
4099 let end = self.span_to_byte_offset(item.span().end());
4100 let original = &self.content[start..end];
4101 original.to_string()
4102 }
4103
4104 fn format_item_enum_variant_only(&self, item: &syn::ItemEnum, variant_name: &str) -> String {
4106 let variant = item.variants.iter()
4108 .find(|v| v.ident.to_string() == variant_name);
4109
4110 if let Some(variant) = variant {
4111 let enum_start = self.span_to_byte_offset(item.span().start());
4113 let variants_start = if !item.variants.is_empty() {
4114 self.span_to_byte_offset(item.variants.first().unwrap().span().start())
4115 } else {
4116 self.span_to_byte_offset(item.span().end())
4117 };
4118
4119 let header = &self.content[enum_start..variants_start].trim_end();
4121
4122 let variant_start = self.span_to_byte_offset(variant.span().start());
4124 let variant_end = self.span_to_byte_offset(variant.span().end());
4125 let variant_source = &self.content[variant_start..variant_end];
4126
4127 format!("{}\n {},\n // ... {} other variant{}\n}}",
4129 header,
4130 variant_source,
4131 item.variants.len() - 1,
4132 if item.variants.len() - 1 == 1 { "" } else { "s" }
4133 )
4134 } else {
4135 self.format_item_enum(item)
4137 }
4138 }
4139
4140 fn format_item_fn(&self, item: &syn::ItemFn) -> String {
4142 let start = self.span_to_byte_offset(item.span().start());
4143 let end = self.span_to_byte_offset(item.span().end());
4144 let original = &self.content[start..end];
4145 original.to_string()
4146 }
4147
4148 fn format_impl_item_fn(&self, item: &syn::ImplItemFn) -> String {
4150 let start = self.span_to_byte_offset(item.span().start());
4151 let end = self.span_to_byte_offset(item.span().end());
4152 let original = &self.content[start..end];
4153 original.to_string()
4154 }
4155
4156 fn format_trait_item_fn(&self, item: &syn::TraitItemFn) -> String {
4158 let start = self.span_to_byte_offset(item.span().start());
4159 let end = self.span_to_byte_offset(item.span().end());
4160 let original = &self.content[start..end];
4161 original.to_string()
4162 }
4163
4164 fn format_item_trait(&self, item: &syn::ItemTrait) -> String {
4166 let start = self.span_to_byte_offset(item.span().start());
4167 let end = self.span_to_byte_offset(item.span().end());
4168 let original = &self.content[start..end];
4169 original.to_string()
4170 }
4171
4172 fn format_item_const(&self, item: &syn::ItemConst) -> String {
4174 let start = self.span_to_byte_offset(item.span().start());
4175 let end = self.span_to_byte_offset(item.span().end());
4176 let original = &self.content[start..end];
4177 original.to_string()
4178 }
4179
4180 fn format_item_static(&self, item: &syn::ItemStatic) -> String {
4182 let start = self.span_to_byte_offset(item.span().start());
4183 let end = self.span_to_byte_offset(item.span().end());
4184 let original = &self.content[start..end];
4185 original.to_string()
4186 }
4187
4188 fn format_item_type(&self, item: &syn::ItemType) -> String {
4190 let start = self.span_to_byte_offset(item.span().start());
4191 let end = self.span_to_byte_offset(item.span().end());
4192 let original = &self.content[start..end];
4193 original.to_string()
4194 }
4195
4196 fn format_item_mod(&self, item: &syn::ItemMod) -> String {
4198 let start = self.span_to_byte_offset(item.span().start());
4199 let end = self.span_to_byte_offset(item.span().end());
4200 let original = &self.content[start..end];
4201 original.to_string()
4202 }
4203
4204 #[allow(dead_code)]
4206 pub(crate) fn find_item_index(&self, node_type: &str, name: &str) -> Result<usize> {
4207 for (index, item) in self.syntax_tree.items.iter().enumerate() {
4208 match (node_type, item) {
4209 ("struct", Item::Struct(s)) if s.ident == name => {
4210 return Ok(index);
4211 }
4212 ("enum", Item::Enum(e)) if e.ident == name => {
4213 return Ok(index);
4214 }
4215 ("fn", Item::Fn(f)) if f.sig.ident == name => {
4216 return Ok(index);
4217 }
4218 ("impl", Item::Impl(impl_block)) => {
4219 if let syn::Type::Path(type_path) = &*impl_block.self_ty {
4221 if let Some(segment) = type_path.path.segments.last() {
4222 if segment.ident == name {
4223 return Ok(index);
4224 }
4225 }
4226 }
4227 }
4228 _ => {}
4229 }
4230 }
4231
4232 anyhow::bail!("Item '{}' of type '{}' not found", name, node_type)
4233 }
4234
4235 #[allow(dead_code)]
4237 pub(crate) fn replace_item_at_index(&mut self, index: usize, new_item: Item) -> Result<()> {
4238 if index >= self.syntax_tree.items.len() {
4239 anyhow::bail!("Index {} out of bounds", index);
4240 }
4241
4242 self.syntax_tree.items[index] = new_item;
4244
4245 self.content = prettyplease::unparse(&self.syntax_tree);
4247
4248 self.line_offsets = Self::compute_line_offsets(&self.content);
4250
4251 Ok(())
4252 }
4253
4254 fn span_to_location(&self, span: Span) -> NodeLocation {
4255 let start = span.start();
4256 let end = span.end();
4257
4258 NodeLocation {
4259 line: start.line,
4260 column: start.column,
4261 end_line: end.line,
4262 end_column: end.column,
4263 }
4264 }
4265
4266 pub(crate) fn transform(&mut self, op: &crate::operations::TransformOp) -> Result<ModificationResult> {
4268 use crate::operations::{InspectResult, TransformAction};
4269
4270 let matches = self.inspect(Some(&op.node_type), op.name_filter.as_deref(), None, false)?;
4272
4273 let filtered_matches: Vec<InspectResult> = if let Some(ref content_filter) = op.content_filter {
4275 matches.into_iter()
4276 .filter(|m| m.snippet.contains(content_filter))
4277 .collect()
4278 } else {
4279 matches
4280 };
4281
4282 if filtered_matches.is_empty() {
4283 return Ok(ModificationResult {
4284 changed: false,
4285 modified_nodes: vec![],
4286 unmatched_qualified_paths: None,
4287 });
4288 }
4289
4290 let mut sorted_matches = filtered_matches;
4293 sorted_matches.sort_by(|a, b| {
4294 b.location.line.cmp(&a.location.line)
4295 .then(b.location.column.cmp(&a.location.column))
4296 });
4297
4298 let mut modified_nodes = Vec::new();
4299
4300 for match_result in &sorted_matches {
4301 let backup_node = BackupNode {
4303 node_type: match_result.node_type.clone(),
4304 identifier: match_result.identifier.clone(),
4305 original_content: match_result.snippet.clone(),
4306 location: match_result.location.clone(),
4307 };
4308
4309 let start_offset = self.line_column_to_byte_offset(
4311 match_result.location.line,
4312 match_result.location.column
4313 )?;
4314 let end_offset = self.line_column_to_byte_offset(
4315 match_result.location.end_line,
4316 match_result.location.end_column
4317 )?;
4318
4319 let original_text = &self.content[start_offset..end_offset];
4321
4322 let replacement = match &op.action {
4324 TransformAction::Comment => {
4325 format!("// {}", original_text.replace("\n", "\n// "))
4327 }
4328 TransformAction::Remove => {
4329 String::new()
4331 }
4332 TransformAction::Replace { with } => {
4333 with.clone()
4335 }
4336 };
4337
4338 self.content.replace_range(start_offset..end_offset, &replacement);
4340
4341 self.line_offsets = Self::compute_line_offsets(&self.content);
4343
4344 modified_nodes.push(backup_node);
4345 }
4346
4347 if !modified_nodes.is_empty() {
4349 }
4353
4354 Ok(ModificationResult {
4355 changed: !modified_nodes.is_empty(),
4356 modified_nodes,
4357 unmatched_qualified_paths: None,
4358 })
4359 }
4360
4361 pub(crate) fn add_call_arg(&mut self, op: &AddCallArgOp) -> Result<ModificationResult> {
4363 let call_matches = self.find_call_sites(&op.call_name, op.call_type.as_deref(), op.content_filter.as_deref())?;
4364
4365 if call_matches.is_empty() {
4366 return Ok(ModificationResult {
4367 changed: false,
4368 modified_nodes: vec![],
4369 unmatched_qualified_paths: None,
4370 });
4371 }
4372
4373 let mut sorted_matches = call_matches;
4375 sorted_matches.sort_by(|a, b| {
4376 b.location.line.cmp(&a.location.line)
4377 .then(b.location.column.cmp(&a.location.column))
4378 });
4379
4380 let mut modified_nodes = Vec::new();
4381
4382 for call_match in &sorted_matches {
4383 let backup_node = BackupNode {
4385 node_type: call_match.node_type.clone(),
4386 identifier: call_match.identifier.clone(),
4387 original_content: call_match.snippet.clone(),
4388 location: call_match.location.clone(),
4389 };
4390
4391 let (args_start, args_end, arg_count) = self.find_call_args_span(&call_match)?;
4393
4394 let insert_idx = match &op.position {
4396 ArgPosition::First => 0,
4397 ArgPosition::Last => arg_count,
4398 ArgPosition::Index(i) => (*i).min(arg_count),
4399 };
4400
4401 let new_arg = if arg_count == 0 {
4403 op.arg_expr.clone()
4405 } else if insert_idx == 0 {
4406 format!("{}, ", op.arg_expr)
4408 } else if insert_idx >= arg_count {
4409 format!(", {}", op.arg_expr)
4411 } else {
4412 format!("{}, ", op.arg_expr)
4414 };
4415
4416 let insert_offset = if arg_count == 0 {
4418 args_start
4420 } else if insert_idx == 0 {
4421 args_start
4423 } else if insert_idx >= arg_count {
4424 args_end
4426 } else {
4427 self.find_arg_boundary_offset(&call_match, insert_idx)?
4429 };
4430
4431 self.content.insert_str(insert_offset, &new_arg);
4433
4434 self.line_offsets = Self::compute_line_offsets(&self.content);
4436
4437 modified_nodes.push(backup_node);
4438 }
4439
4440 Ok(ModificationResult {
4441 changed: !modified_nodes.is_empty(),
4442 modified_nodes,
4443 unmatched_qualified_paths: None,
4444 })
4445 }
4446
4447 pub(crate) fn update_call_arg(&mut self, op: &UpdateCallArgOp) -> Result<ModificationResult> {
4449 let call_matches = self.find_call_sites(&op.call_name, op.call_type.as_deref(), op.content_filter.as_deref())?;
4450
4451 if call_matches.is_empty() {
4452 return Ok(ModificationResult {
4453 changed: false,
4454 modified_nodes: vec![],
4455 unmatched_qualified_paths: None,
4456 });
4457 }
4458
4459 let mut sorted_matches = call_matches;
4461 sorted_matches.sort_by(|a, b| {
4462 b.location.line.cmp(&a.location.line)
4463 .then(b.location.column.cmp(&a.location.column))
4464 });
4465
4466 let mut modified_nodes = Vec::new();
4467
4468 for call_match in &sorted_matches {
4469 let (_, _, arg_count) = self.find_call_args_span(&call_match)?;
4471
4472 if op.arg_index >= arg_count {
4474 continue; }
4476
4477 let backup_node = BackupNode {
4479 node_type: call_match.node_type.clone(),
4480 identifier: call_match.identifier.clone(),
4481 original_content: call_match.snippet.clone(),
4482 location: call_match.location.clone(),
4483 };
4484
4485 let (arg_start, arg_end) = self.find_arg_span(&call_match, op.arg_index)?;
4487
4488 self.content.replace_range(arg_start..arg_end, &op.new_expr);
4490
4491 self.line_offsets = Self::compute_line_offsets(&self.content);
4493
4494 modified_nodes.push(backup_node);
4495 }
4496
4497 Ok(ModificationResult {
4498 changed: !modified_nodes.is_empty(),
4499 modified_nodes,
4500 unmatched_qualified_paths: None,
4501 })
4502 }
4503
4504 pub(crate) fn remove_call_arg(&mut self, op: &RemoveCallArgOp) -> Result<ModificationResult> {
4506 let call_matches = self.find_call_sites(&op.call_name, op.call_type.as_deref(), op.content_filter.as_deref())?;
4507
4508 if call_matches.is_empty() {
4509 return Ok(ModificationResult {
4510 changed: false,
4511 modified_nodes: vec![],
4512 unmatched_qualified_paths: None,
4513 });
4514 }
4515
4516 let mut sorted_matches = call_matches;
4518 sorted_matches.sort_by(|a, b| {
4519 b.location.line.cmp(&a.location.line)
4520 .then(b.location.column.cmp(&a.location.column))
4521 });
4522
4523 let mut modified_nodes = Vec::new();
4524
4525 for call_match in &sorted_matches {
4526 let (_, _, arg_count) = self.find_call_args_span(&call_match)?;
4528
4529 if op.arg_index >= arg_count {
4531 continue; }
4533
4534 let backup_node = BackupNode {
4536 node_type: call_match.node_type.clone(),
4537 identifier: call_match.identifier.clone(),
4538 original_content: call_match.snippet.clone(),
4539 location: call_match.location.clone(),
4540 };
4541
4542 let (remove_start, remove_end) = self.find_arg_span_with_comma(&call_match, op.arg_index, arg_count)?;
4544
4545 self.content.replace_range(remove_start..remove_end, "");
4547
4548 self.line_offsets = Self::compute_line_offsets(&self.content);
4550
4551 modified_nodes.push(backup_node);
4552 }
4553
4554 Ok(ModificationResult {
4555 changed: !modified_nodes.is_empty(),
4556 modified_nodes,
4557 unmatched_qualified_paths: None,
4558 })
4559 }
4560
4561 fn find_call_sites(&self, call_name: &str, call_type: Option<&str>, content_filter: Option<&str>) -> Result<Vec<InspectResult>> {
4563 let mut results = Vec::new();
4564
4565 let search_functions = call_type.map_or(true, |ct| ct == "function");
4567 let search_methods = call_type.map_or(true, |ct| ct == "method");
4568
4569 if search_functions {
4570 let func_results = self.inspect(Some("function-call"), Some(call_name), None, false)?;
4571 results.extend(func_results);
4572 }
4573
4574 if search_methods {
4575 let method_results = self.inspect(Some("method-call"), Some(call_name), None, false)?;
4576 results.extend(method_results);
4577 }
4578
4579 if let Some(filter) = content_filter {
4581 results = results.into_iter()
4582 .filter(|r| r.snippet.contains(filter))
4583 .collect();
4584 }
4585
4586 Ok(results)
4587 }
4588
4589 fn find_call_args_span(&self, call_match: &InspectResult) -> Result<(usize, usize, usize)> {
4592 let start_offset = self.line_column_to_byte_offset(
4593 call_match.location.line,
4594 call_match.location.column
4595 )?;
4596 let end_offset = self.line_column_to_byte_offset(
4597 call_match.location.end_line,
4598 call_match.location.end_column
4599 )?;
4600
4601 let call_text = &self.content[start_offset..end_offset];
4602
4603 let paren_start = call_text.find('(')
4605 .ok_or_else(|| anyhow::anyhow!("Could not find opening parenthesis in call"))?;
4606
4607 let paren_end = self.find_matching_paren(call_text, paren_start)?;
4609
4610 let args_text = &call_text[paren_start + 1..paren_end];
4612
4613 let arg_count = self.count_args(args_text);
4615
4616 Ok((
4617 start_offset + paren_start + 1,
4618 start_offset + paren_end,
4619 arg_count
4620 ))
4621 }
4622
4623 fn find_matching_paren(&self, text: &str, open_pos: usize) -> Result<usize> {
4625 let bytes = text.as_bytes();
4626 let mut depth = 0;
4627 let mut in_string = false;
4628 let mut string_char = '"';
4629 let mut escape_next = false;
4630
4631 for (i, &ch) in bytes.iter().enumerate().skip(open_pos) {
4632 if escape_next {
4633 escape_next = false;
4634 continue;
4635 }
4636
4637 if ch == b'\\' && in_string {
4638 escape_next = true;
4639 continue;
4640 }
4641
4642 if (ch == b'"' || ch == b'\'') && !in_string {
4643 in_string = true;
4644 string_char = ch as char;
4645 continue;
4646 }
4647
4648 if in_string && ch == string_char as u8 {
4649 in_string = false;
4650 continue;
4651 }
4652
4653 if in_string {
4654 continue;
4655 }
4656
4657 match ch {
4658 b'(' | b'[' | b'{' => depth += 1,
4659 b')' => {
4660 depth -= 1;
4661 if depth == 0 {
4662 return Ok(i);
4663 }
4664 }
4665 b']' | b'}' => depth -= 1,
4666 _ => {}
4667 }
4668 }
4669
4670 anyhow::bail!("Could not find matching closing parenthesis")
4671 }
4672
4673 fn count_args(&self, args_text: &str) -> usize {
4675 let trimmed = args_text.trim();
4676 if trimmed.is_empty() {
4677 return 0;
4678 }
4679
4680 let mut count = 1;
4681 let mut depth = 0;
4682 let mut in_string = false;
4683 let mut string_char = '"';
4684 let mut escape_next = false;
4685
4686 for ch in trimmed.chars() {
4687 if escape_next {
4688 escape_next = false;
4689 continue;
4690 }
4691
4692 if ch == '\\' && in_string {
4693 escape_next = true;
4694 continue;
4695 }
4696
4697 if (ch == '"' || ch == '\'') && !in_string {
4698 in_string = true;
4699 string_char = ch;
4700 continue;
4701 }
4702
4703 if in_string && ch == string_char {
4704 in_string = false;
4705 continue;
4706 }
4707
4708 if in_string {
4709 continue;
4710 }
4711
4712 match ch {
4713 '(' | '[' | '{' | '<' => depth += 1,
4714 ')' | ']' | '}' | '>' => depth -= 1,
4715 ',' if depth == 0 => count += 1,
4716 _ => {}
4717 }
4718 }
4719
4720 count
4721 }
4722
4723 fn find_arg_boundary_offset(&self, call_match: &InspectResult, arg_index: usize) -> Result<usize> {
4725 let (args_start, args_end, _) = self.find_call_args_span(call_match)?;
4726 let args_text = &self.content[args_start..args_end];
4727
4728 let mut current_arg = 0;
4729 let mut depth = 0;
4730 let mut in_string = false;
4731 let mut string_char = '"';
4732 let mut escape_next = false;
4733
4734 for (i, ch) in args_text.char_indices() {
4735 if escape_next {
4736 escape_next = false;
4737 continue;
4738 }
4739
4740 if ch == '\\' && in_string {
4741 escape_next = true;
4742 continue;
4743 }
4744
4745 if (ch == '"' || ch == '\'') && !in_string {
4746 in_string = true;
4747 string_char = ch;
4748 continue;
4749 }
4750
4751 if in_string && ch == string_char {
4752 in_string = false;
4753 continue;
4754 }
4755
4756 if in_string {
4757 continue;
4758 }
4759
4760 match ch {
4761 '(' | '[' | '{' | '<' => depth += 1,
4762 ')' | ']' | '}' | '>' => depth -= 1,
4763 ',' if depth == 0 => {
4764 current_arg += 1;
4765 if current_arg == arg_index {
4766 let rest = &args_text[i + 1..];
4768 let whitespace_len = rest.len() - rest.trim_start().len();
4769 return Ok(args_start + i + 1 + whitespace_len);
4770 }
4771 }
4772 _ => {}
4773 }
4774 }
4775
4776 Ok(args_end)
4778 }
4779
4780 fn find_arg_span(&self, call_match: &InspectResult, arg_index: usize) -> Result<(usize, usize)> {
4782 let (args_start, args_end, _) = self.find_call_args_span(call_match)?;
4783 let args_text = &self.content[args_start..args_end];
4784
4785 let mut current_arg = 0;
4786 let mut arg_start_in_text = 0;
4787 let mut depth = 0;
4788 let mut in_string = false;
4789 let mut string_char = '"';
4790 let mut escape_next = false;
4791
4792 let trimmed_start = args_text.len() - args_text.trim_start().len();
4794 if arg_index == 0 {
4795 arg_start_in_text = trimmed_start;
4796 }
4797
4798 for (i, ch) in args_text.char_indices() {
4799 if escape_next {
4800 escape_next = false;
4801 continue;
4802 }
4803
4804 if ch == '\\' && in_string {
4805 escape_next = true;
4806 continue;
4807 }
4808
4809 if (ch == '"' || ch == '\'') && !in_string {
4810 in_string = true;
4811 string_char = ch;
4812 continue;
4813 }
4814
4815 if in_string && ch == string_char {
4816 in_string = false;
4817 continue;
4818 }
4819
4820 if in_string {
4821 continue;
4822 }
4823
4824 match ch {
4825 '(' | '[' | '{' | '<' => depth += 1,
4826 ')' | ']' | '}' | '>' => depth -= 1,
4827 ',' if depth == 0 => {
4828 if current_arg == arg_index {
4829 let arg_text = &args_text[arg_start_in_text..i];
4832 let trimmed_len = arg_text.trim_end().len();
4833 return Ok((args_start + arg_start_in_text, args_start + arg_start_in_text + trimmed_len));
4834 }
4835 current_arg += 1;
4836 let rest = &args_text[i + 1..];
4838 let whitespace_len = rest.len() - rest.trim_start().len();
4839 arg_start_in_text = i + 1 + whitespace_len;
4840 }
4841 _ => {}
4842 }
4843 }
4844
4845 if current_arg == arg_index {
4847 let arg_text = &args_text[arg_start_in_text..];
4848 let trimmed_len = arg_text.trim_end().len();
4849 return Ok((args_start + arg_start_in_text, args_start + arg_start_in_text + trimmed_len));
4850 }
4851
4852 anyhow::bail!("Argument index {} not found", arg_index)
4853 }
4854
4855 fn find_arg_span_with_comma(&self, call_match: &InspectResult, arg_index: usize, arg_count: usize) -> Result<(usize, usize)> {
4857 let (args_start, args_end, _) = self.find_call_args_span(call_match)?;
4858 let args_text = &self.content[args_start..args_end];
4859
4860 let mut current_arg = 0;
4861 let mut arg_start_in_text = 0;
4862 let mut depth = 0;
4863 let mut in_string = false;
4864 let mut string_char = '"';
4865 let mut escape_next = false;
4866
4867 let trimmed_start = args_text.len() - args_text.trim_start().len();
4869 if arg_index == 0 {
4870 arg_start_in_text = trimmed_start;
4871 }
4872
4873 for (i, ch) in args_text.char_indices() {
4874 if escape_next {
4875 escape_next = false;
4876 continue;
4877 }
4878
4879 if ch == '\\' && in_string {
4880 escape_next = true;
4881 continue;
4882 }
4883
4884 if (ch == '"' || ch == '\'') && !in_string {
4885 in_string = true;
4886 string_char = ch;
4887 continue;
4888 }
4889
4890 if in_string && ch == string_char {
4891 in_string = false;
4892 continue;
4893 }
4894
4895 if in_string {
4896 continue;
4897 }
4898
4899 match ch {
4900 '(' | '[' | '{' | '<' => depth += 1,
4901 ')' | ']' | '}' | '>' => depth -= 1,
4902 ',' if depth == 0 => {
4903 if current_arg == arg_index {
4904 if arg_index == 0 && arg_count > 1 {
4906 let rest = &args_text[i + 1..];
4908 let whitespace_len = rest.len() - rest.trim_start().len();
4909 return Ok((args_start + arg_start_in_text, args_start + i + 1 + whitespace_len));
4910 } else {
4911 return Ok((args_start + arg_start_in_text, args_start + i + 1));
4913 }
4914 }
4915 current_arg += 1;
4916 let rest = &args_text[i + 1..];
4918 let whitespace_len = rest.len() - rest.trim_start().len();
4919 arg_start_in_text = i + 1 + whitespace_len;
4920 }
4921 _ => {}
4922 }
4923 }
4924
4925 if current_arg == arg_index {
4927 if arg_count > 1 {
4928 let before_text = &args_text[..arg_start_in_text];
4931 let comma_pos = before_text.rfind(',')
4932 .ok_or_else(|| anyhow::anyhow!("Could not find comma before argument"))?;
4933 return Ok((args_start + comma_pos, args_start + args_text.trim_end().len()));
4934 } else {
4935 let trimmed_end = args_text.trim_end().len();
4937 return Ok((args_start + arg_start_in_text, args_start + trimmed_end));
4938 }
4939 }
4940
4941 anyhow::bail!("Argument index {} not found", arg_index)
4942 }
4943
4944 pub(crate) fn rename_enum_variant(&mut self, op: &crate::operations::RenameEnumVariantOp) -> Result<ModificationResult> {
4946 use crate::operations::EditMode;
4947
4948 let path_resolver = if let Some(enum_path) = &op.enum_path {
4950 let mut resolver = PathResolver::new(enum_path)
4951 .ok_or_else(|| anyhow::anyhow!("Invalid enum path: {}", enum_path))?;
4952
4953 resolver.scan_file(&self.syntax_tree);
4955 Some(resolver)
4956 } else {
4957 None
4958 };
4959
4960 match op.edit_mode {
4961 EditMode::Surgical => {
4962 use syn::visit::Visit;
4964 use crate::surgical::Replacement;
4965
4966 let mut collector = EnumVariantReplacementCollector {
4967 enum_name: op.enum_name.clone(),
4968 old_variant: op.old_variant.clone(),
4969 new_variant: op.new_variant.clone(),
4970 path_resolver,
4971 replacements: Vec::new(),
4972 };
4973
4974 collector.visit_file(&self.syntax_tree);
4975
4976 if collector.replacements.is_empty() {
4977 return Ok(ModificationResult {
4978 changed: false,
4979 modified_nodes: vec![],
4980 unmatched_qualified_paths: None,
4981 });
4982 }
4983
4984 self.content = crate::surgical::apply_surgical_edits(&self.content, collector.replacements);
4986
4987 self.line_offsets = Self::compute_line_offsets(&self.content);
4989
4990 self.syntax_tree = syn::parse_str(&self.content)
4992 .context("Failed to re-parse after surgical edit")?;
4993
4994 let backup_node = BackupNode {
4995 node_type: "EnumVariantRename".to_string(),
4996 identifier: format!("{}::{} -> {} (surgical)", op.enum_name, op.old_variant, op.new_variant),
4997 original_content: format!("Renamed {} to {} in enum {} (surgical mode)", op.old_variant, op.new_variant, op.enum_name),
4998 location: NodeLocation {
4999 line: 1,
5000 column: 0,
5001 end_line: 1,
5002 end_column: 0,
5003 },
5004 };
5005
5006 Ok(ModificationResult {
5007 changed: true,
5008 modified_nodes: vec![backup_node],
5009 unmatched_qualified_paths: None,
5010 })
5011 }
5012 EditMode::Reformat => {
5013 let mut renamer = EnumVariantRenamer {
5015 enum_name: op.enum_name.clone(),
5016 old_variant: op.old_variant.clone(),
5017 new_variant: op.new_variant.clone(),
5018 path_resolver,
5019 modified: false,
5020 };
5021
5022 renamer.visit_file_mut(&mut self.syntax_tree);
5024
5025 if !renamer.modified {
5026 return Ok(ModificationResult {
5027 changed: false,
5028 modified_nodes: vec![],
5029 unmatched_qualified_paths: None,
5030 });
5031 }
5032
5033 self.content = prettyplease::unparse(&self.syntax_tree);
5035
5036 self.line_offsets = Self::compute_line_offsets(&self.content);
5038
5039 let backup_node = BackupNode {
5041 node_type: "EnumVariantRename".to_string(),
5042 identifier: format!("{}::{} -> {}", op.enum_name, op.old_variant, op.new_variant),
5043 original_content: format!("Renamed {} to {} in enum {}", op.old_variant, op.new_variant, op.enum_name),
5044 location: NodeLocation {
5045 line: 1,
5046 column: 0,
5047 end_line: 1,
5048 end_column: 0,
5049 },
5050 };
5051
5052 Ok(ModificationResult {
5053 changed: true,
5054 modified_nodes: vec![backup_node],
5055 unmatched_qualified_paths: None,
5056 })
5057 }
5058 }
5059 }
5060
5061 pub(crate) fn rename_function(&mut self, op: &crate::operations::RenameFunctionOp) -> Result<ModificationResult> {
5063 use crate::operations::EditMode;
5064
5065 let path_resolver = if let Some(function_path) = &op.function_path {
5067 let mut resolver = PathResolver::new(function_path)
5068 .ok_or_else(|| anyhow::anyhow!("Invalid function path: {}", function_path))?;
5069
5070 resolver.scan_file(&self.syntax_tree);
5072 Some(resolver)
5073 } else {
5074 None
5075 };
5076
5077 match op.edit_mode {
5078 EditMode::Surgical => {
5079 use syn::visit::Visit;
5081
5082 let mut collector = FunctionReplacementCollector {
5083 old_name: op.old_name.clone(),
5084 new_name: op.new_name.clone(),
5085 path_resolver,
5086 replacements: Vec::new(),
5087 };
5088
5089 collector.visit_file(&self.syntax_tree);
5090
5091 if collector.replacements.is_empty() {
5092 return Ok(ModificationResult {
5093 changed: false,
5094 modified_nodes: vec![],
5095 unmatched_qualified_paths: None,
5096 });
5097 }
5098
5099 self.content = crate::surgical::apply_surgical_edits(&self.content, collector.replacements);
5101
5102 self.line_offsets = Self::compute_line_offsets(&self.content);
5104
5105 self.syntax_tree = syn::parse_str(&self.content)
5107 .context("Failed to re-parse after surgical edit")?;
5108
5109 let backup_node = BackupNode {
5110 node_type: "FunctionRename".to_string(),
5111 identifier: format!("{} -> {} (surgical)", op.old_name, op.new_name),
5112 original_content: format!("Renamed {} to {} (surgical mode)", op.old_name, op.new_name),
5113 location: NodeLocation {
5114 line: 1,
5115 column: 0,
5116 end_line: 1,
5117 end_column: 0,
5118 },
5119 };
5120
5121 Ok(ModificationResult {
5122 changed: true,
5123 modified_nodes: vec![backup_node],
5124 unmatched_qualified_paths: None,
5125 })
5126 }
5127 EditMode::Reformat => {
5128 let mut renamer = FunctionRenamer {
5130 old_name: op.old_name.clone(),
5131 new_name: op.new_name.clone(),
5132 path_resolver,
5133 modified: false,
5134 };
5135
5136 renamer.visit_file_mut(&mut self.syntax_tree);
5138
5139 if !renamer.modified {
5140 return Ok(ModificationResult {
5141 changed: false,
5142 modified_nodes: vec![],
5143 unmatched_qualified_paths: None,
5144 });
5145 }
5146
5147 self.content = prettyplease::unparse(&self.syntax_tree);
5149
5150 self.line_offsets = Self::compute_line_offsets(&self.content);
5152
5153 let backup_node = BackupNode {
5155 node_type: "FunctionRename".to_string(),
5156 identifier: format!("{} -> {}", op.old_name, op.new_name),
5157 original_content: format!("Renamed {} to {}", op.old_name, op.new_name),
5158 location: NodeLocation {
5159 line: 1,
5160 column: 0,
5161 end_line: 1,
5162 end_column: 0,
5163 },
5164 };
5165
5166 Ok(ModificationResult {
5167 changed: true,
5168 modified_nodes: vec![backup_node],
5169 unmatched_qualified_paths: None,
5170 })
5171 }
5172 }
5173 }
5174
5175 fn line_column_to_byte_offset(&self, line: usize, column: usize) -> Result<usize> {
5177 if line == 0 || line > self.line_offsets.len() {
5178 anyhow::bail!("Line {} out of range", line);
5179 }
5180
5181 let line_start = self.line_offsets[line - 1];
5182 Ok(line_start + column)
5183 }
5184}
5185
5186struct MatchArmAdder {
5188 target_function: Option<String>,
5189 arm_to_add: Arm,
5190 modified: bool,
5191 current_function: Option<String>,
5192 modified_function: Option<String>,
5193}
5194
5195impl VisitMut for MatchArmAdder {
5196 fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
5197 let prev_fn = self.current_function.clone();
5198 self.current_function = Some(node.sig.ident.to_string());
5199
5200 syn::visit_mut::visit_item_fn_mut(self, node);
5202
5203 self.current_function = prev_fn;
5204 }
5205
5206 fn visit_expr_match_mut(&mut self, node: &mut ExprMatch) {
5207 if let Some(ref target) = self.target_function {
5209 if self.current_function.as_ref() != Some(target) {
5210 syn::visit_mut::visit_expr_match_mut(self, node);
5212 return;
5213 }
5214 }
5215
5216 let pattern_str = self.arm_to_add.pat.to_token_stream().to_string();
5218 let already_exists = node.arms.iter().any(|arm| {
5219 arm.pat.to_token_stream().to_string() == pattern_str
5220 });
5221
5222 if !already_exists {
5223 node.arms.push(self.arm_to_add.clone());
5225 self.modified = true;
5226 self.modified_function = self.current_function.clone();
5227 }
5228
5229 syn::visit_mut::visit_expr_match_mut(self, node);
5231 }
5232}
5233
5234struct MatchArmUpdater {
5236 target_function: Option<String>,
5237 pattern_to_match: String,
5238 new_body: syn::Expr,
5239 modified: bool,
5240 current_function: Option<String>,
5241 modified_function: Option<String>,
5242}
5243
5244impl VisitMut for MatchArmUpdater {
5245 fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
5246 let prev_fn = self.current_function.clone();
5247 self.current_function = Some(node.sig.ident.to_string());
5248
5249 syn::visit_mut::visit_item_fn_mut(self, node);
5250
5251 self.current_function = prev_fn;
5252 }
5253
5254 fn visit_expr_match_mut(&mut self, node: &mut ExprMatch) {
5255 if let Some(ref target) = self.target_function {
5257 if self.current_function.as_ref() != Some(target) {
5258 syn::visit_mut::visit_expr_match_mut(self, node);
5259 return;
5260 }
5261 }
5262
5263 for arm in &mut node.arms {
5265 let pattern_str = arm.pat.to_token_stream().to_string();
5266 let pattern_normalized = pattern_str.replace(" ", "");
5268 let target_normalized = self.pattern_to_match.replace(" ", "");
5269
5270 if pattern_normalized == target_normalized {
5271 arm.body = Box::new(self.new_body.clone());
5272 self.modified = true;
5273 self.modified_function = self.current_function.clone();
5274 break;
5275 }
5276 }
5277
5278 syn::visit_mut::visit_expr_match_mut(self, node);
5279 }
5280}
5281
5282struct MatchArmRemover {
5284 target_function: Option<String>,
5285 pattern_to_remove: String,
5286 modified: bool,
5287 current_function: Option<String>,
5288 modified_function: Option<String>,
5289}
5290
5291impl VisitMut for MatchArmRemover {
5292 fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
5293 let prev_fn = self.current_function.clone();
5294 self.current_function = Some(node.sig.ident.to_string());
5295
5296 syn::visit_mut::visit_item_fn_mut(self, node);
5297
5298 self.current_function = prev_fn;
5299 }
5300
5301 fn visit_expr_match_mut(&mut self, node: &mut ExprMatch) {
5302 if let Some(ref target) = self.target_function {
5304 if self.current_function.as_ref() != Some(target) {
5305 syn::visit_mut::visit_expr_match_mut(self, node);
5306 return;
5307 }
5308 }
5309
5310 let mut index_to_remove = None;
5312 for (i, arm) in node.arms.iter().enumerate() {
5313 let pattern_str = arm.pat.to_token_stream().to_string();
5314 let pattern_normalized = pattern_str.replace(" ", "");
5316 let target_normalized = self.pattern_to_remove.replace(" ", "");
5317
5318 if pattern_normalized == target_normalized {
5319 index_to_remove = Some(i);
5320 break;
5321 }
5322 }
5323
5324 if let Some(index) = index_to_remove {
5325 node.arms.remove(index);
5326 self.modified = true;
5327 self.modified_function = self.current_function.clone();
5328 }
5329
5330 syn::visit_mut::visit_expr_match_mut(self, node);
5331 }
5332}
5333
5334struct MultiMatchArmAdder {
5336 target_function: Option<String>,
5337 arms_to_add: Vec<(String, Arm)>, modified: bool,
5339 current_function: Option<String>,
5340 modified_function: Option<String>,
5341}
5342
5343impl VisitMut for MultiMatchArmAdder {
5344 fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
5345 let prev_fn = self.current_function.clone();
5346 self.current_function = Some(node.sig.ident.to_string());
5347
5348 syn::visit_mut::visit_item_fn_mut(self, node);
5349
5350 self.current_function = prev_fn;
5351 }
5352
5353 fn visit_expr_match_mut(&mut self, node: &mut ExprMatch) {
5354 if let Some(ref target) = self.target_function {
5356 if self.current_function.as_ref() != Some(target) {
5357 syn::visit_mut::visit_expr_match_mut(self, node);
5358 return;
5359 }
5360 }
5361
5362 for (pattern_str, arm) in &self.arms_to_add {
5364 let already_exists = node.arms.iter().any(|existing_arm| {
5366 existing_arm.pat.to_token_stream().to_string() == *pattern_str
5367 });
5368
5369 if !already_exists {
5370 node.arms.push(arm.clone());
5371 self.modified = true;
5372 self.modified_function = self.current_function.clone();
5373 }
5374 }
5375
5376 syn::visit_mut::visit_expr_match_mut(self, node);
5377 }
5378}
5379
5380struct StructLiteralFieldAdder {
5382 struct_name: String,
5383 field_def: String,
5384 field_name: String,
5385 position: InsertPosition,
5386 path_resolver: Option<PathResolver>,
5387 modified: bool,
5388}
5389
5390impl VisitMut for StructLiteralFieldAdder {
5391 fn visit_expr_mut(&mut self, node: &mut Expr) {
5392 if let Expr::Struct(expr_struct) = node {
5394 let is_match = if let Some(resolver) = &self.path_resolver {
5395 resolver.matches_target(&expr_struct.path)
5397 } else {
5398 if self.struct_name.contains("::") {
5404 if self.struct_name.starts_with("*::") {
5406 let target_name = &self.struct_name[3..]; expr_struct.path.segments.last()
5409 .map(|seg| seg.ident.to_string() == target_name)
5410 .unwrap_or(false)
5411 } else {
5412 let path_str = expr_struct.path.segments.iter()
5414 .map(|seg| seg.ident.to_string())
5415 .collect::<Vec<_>>()
5416 .join("::");
5417 path_str == self.struct_name
5418 }
5419 } else {
5420 expr_struct.path.segments.len() == 1
5422 && expr_struct.path.segments.last()
5423 .map(|seg| seg.ident.to_string())
5424 .as_ref() == Some(&self.struct_name)
5425 }
5426 };
5427
5428 if is_match {
5429 let field_exists = expr_struct.fields.iter().any(|fv| {
5431 fv.member.to_token_stream().to_string() == self.field_name
5432 });
5433
5434 if !field_exists {
5435 let field_value_code = format!("{{ {} }}", self.field_def);
5438 if let Ok(expr) = parse_str::<ExprStruct>(&format!("Dummy {}", field_value_code)) {
5439 if let Some(new_fv) = expr.fields.first() {
5440 match &self.position {
5442 InsertPosition::First => {
5443 expr_struct.fields.insert(0, new_fv.clone());
5444 self.modified = true;
5445 }
5446 InsertPosition::Last => {
5447 expr_struct.fields.push(new_fv.clone());
5448 self.modified = true;
5449 }
5450 InsertPosition::After(after_field) => {
5451 if let Some(pos) = expr_struct.fields.iter().position(|fv| {
5453 fv.member.to_token_stream().to_string() == *after_field
5454 }) {
5455 expr_struct.fields.insert(pos + 1, new_fv.clone());
5456 self.modified = true;
5457 }
5458 }
5459 InsertPosition::Before(before_field) => {
5460 if let Some(pos) = expr_struct.fields.iter().position(|fv| {
5462 fv.member.to_token_stream().to_string() == *before_field
5463 }) {
5464 expr_struct.fields.insert(pos, new_fv.clone());
5465 self.modified = true;
5466 }
5467 }
5468 }
5469 }
5470 }
5471 }
5472 }
5473 }
5474
5475 syn::visit_mut::visit_expr_mut(self, node);
5478 }
5479}
5480
5481struct EnumVariantRenamer {
5483 enum_name: String,
5484 old_variant: String,
5485 new_variant: String,
5486 path_resolver: Option<PathResolver>,
5487 modified: bool,
5488}
5489
5490impl EnumVariantRenamer {
5491 fn rename_path(&mut self, path: &mut syn::Path) {
5501 let segments: Vec<_> = path.segments.iter().collect();
5503 let len = segments.len();
5504
5505 if len >= 2 {
5506 let potential_variant = &segments[len - 1];
5508 let potential_enum = &segments[len - 2];
5509
5510 if potential_enum.ident == self.enum_name
5511 && potential_variant.ident == self.old_variant
5512 {
5513 if let Some(resolver) = &self.path_resolver {
5517 let enum_path = syn::Path {
5519 leading_colon: path.leading_colon,
5520 segments: path.segments.iter()
5521 .take(len - 1)
5522 .cloned()
5523 .collect(),
5524 };
5525
5526 if resolver.matches_target(&enum_path) {
5528 path.segments[len - 1].ident = syn::Ident::new(
5529 &self.new_variant,
5530 path.segments[len - 1].ident.span()
5531 );
5532 self.modified = true;
5533 }
5534 } else {
5535 if len == 2 {
5538 path.segments[1].ident = syn::Ident::new(
5539 &self.new_variant,
5540 path.segments[1].ident.span()
5541 );
5542 self.modified = true;
5543 }
5544 }
5545 }
5546 } else if len == 1 {
5547 if segments[0].ident == self.old_variant {
5549 if self.path_resolver.is_none() {
5553 path.segments[0].ident = syn::Ident::new(
5554 &self.new_variant,
5555 path.segments[0].ident.span()
5556 );
5557 self.modified = true;
5558 }
5559 }
5560 }
5561 }
5562}
5563
5564impl VisitMut for EnumVariantRenamer {
5565 fn visit_item_enum_mut(&mut self, node: &mut syn::ItemEnum) {
5567 if node.ident == self.enum_name {
5568 for variant in &mut node.variants {
5569 if variant.ident == self.old_variant {
5570 variant.ident = syn::Ident::new(&self.new_variant, variant.ident.span());
5571 self.modified = true;
5572 }
5573 }
5574 }
5575
5576 syn::visit_mut::visit_item_enum_mut(self, node);
5578 }
5579
5580 fn visit_pat_mut(&mut self, pat: &mut syn::Pat) {
5582 match pat {
5583 syn::Pat::TupleStruct(tuple_struct) => {
5584 self.rename_path(&mut tuple_struct.path);
5585 }
5586 syn::Pat::Struct(struct_pat) => {
5587 self.rename_path(&mut struct_pat.path);
5588 }
5589 syn::Pat::Path(path_pat) => {
5590 self.rename_path(&mut path_pat.path);
5591 }
5592 _ => {}
5593 }
5594
5595 syn::visit_mut::visit_pat_mut(self, pat);
5597 }
5598
5599 fn visit_expr_mut(&mut self, expr: &mut syn::Expr) {
5601 match expr {
5602 syn::Expr::Path(expr_path) => {
5603 self.rename_path(&mut expr_path.path);
5604 }
5605 syn::Expr::Call(call) => {
5606 if let syn::Expr::Path(path) = &mut *call.func {
5607 self.rename_path(&mut path.path);
5608 }
5609 }
5610 syn::Expr::Struct(struct_expr) => {
5611 self.rename_path(&mut struct_expr.path);
5612 }
5613 _ => {}
5614 }
5615
5616 syn::visit_mut::visit_expr_mut(self, expr);
5618 }
5619}
5620
5621struct EnumVariantReplacementCollector {
5623 enum_name: String,
5624 old_variant: String,
5625 new_variant: String,
5626 path_resolver: Option<PathResolver>,
5627 replacements: Vec<crate::surgical::Replacement>,
5628}
5629
5630impl EnumVariantReplacementCollector {
5631 fn collect_path_replacement(&mut self, path: &syn::Path) {
5633 let segments: Vec<_> = path.segments.iter().collect();
5634 let len = segments.len();
5635
5636 if len >= 2 {
5637 let potential_variant = &segments[len - 1];
5638 let potential_enum = &segments[len - 2];
5639
5640 if potential_enum.ident == self.enum_name
5641 && potential_variant.ident == self.old_variant
5642 {
5643 let should_rename = if let Some(resolver) = &self.path_resolver {
5647 let enum_path = syn::Path {
5648 leading_colon: path.leading_colon,
5649 segments: path.segments.iter()
5650 .take(len - 1)
5651 .cloned()
5652 .collect(),
5653 };
5654 resolver.matches_target(&enum_path)
5655 } else {
5656 len == 2
5658 };
5659
5660 if should_rename {
5661 let span = potential_variant.ident.span();
5662 let start = span.start();
5663 let end = span.end();
5664
5665 self.replacements.push(crate::surgical::Replacement::new(
5666 start,
5667 end,
5668 self.new_variant.clone(),
5669 ));
5670 }
5671 }
5672 } else if len == 1 && self.path_resolver.is_none() {
5673 if segments[0].ident == self.old_variant {
5675 let span = segments[0].ident.span();
5676 let start = span.start();
5677 let end = span.end();
5678
5679 self.replacements.push(crate::surgical::Replacement::new(
5680 start,
5681 end,
5682 self.new_variant.clone(),
5683 ));
5684 }
5685 }
5686 }
5687}
5688
5689impl<'ast> syn::visit::Visit<'ast> for EnumVariantReplacementCollector {
5690 fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
5691 if node.ident == self.enum_name {
5692 for variant in &node.variants {
5693 if variant.ident == self.old_variant {
5694 let span = variant.ident.span();
5695 let start = span.start();
5696 let end = span.end();
5697
5698 self.replacements.push(crate::surgical::Replacement::new(
5699 start,
5700 end,
5701 self.new_variant.clone(),
5702 ));
5703 }
5704 }
5705 }
5706 syn::visit::visit_item_enum(self, node);
5707 }
5708
5709 fn visit_pat(&mut self, pat: &'ast syn::Pat) {
5710 match pat {
5711 syn::Pat::TupleStruct(tuple_struct) => {
5712 self.collect_path_replacement(&tuple_struct.path);
5713 }
5714 syn::Pat::Struct(struct_pat) => {
5715 self.collect_path_replacement(&struct_pat.path);
5716 }
5717 syn::Pat::Path(path_pat) => {
5718 self.collect_path_replacement(&path_pat.path);
5719 }
5720 _ => {}
5721 }
5722 syn::visit::visit_pat(self, pat);
5723 }
5724
5725 fn visit_expr(&mut self, expr: &'ast syn::Expr) {
5726 match expr {
5727 syn::Expr::Path(expr_path) => {
5728 self.collect_path_replacement(&expr_path.path);
5729 }
5730 syn::Expr::Call(call) => {
5731 if let syn::Expr::Path(path) = &*call.func {
5732 self.collect_path_replacement(&path.path);
5733 }
5734 }
5735 syn::Expr::Struct(struct_expr) => {
5736 self.collect_path_replacement(&struct_expr.path);
5737 }
5738 _ => {}
5739 }
5740 syn::visit::visit_expr(self, expr);
5741 }
5742}
5743
5744struct FunctionRenamer {
5746 old_name: String,
5747 new_name: String,
5748 path_resolver: Option<PathResolver>,
5749 modified: bool,
5750}
5751
5752impl FunctionRenamer {
5753 fn rename_ident(&mut self, ident: &mut syn::Ident) {
5755 if ident == &self.old_name {
5756 *ident = syn::Ident::new(&self.new_name, ident.span());
5757 self.modified = true;
5758 }
5759 }
5760
5761 fn matches_target_function(&self, path: &syn::Path) -> bool {
5763 if let Some(resolver) = &self.path_resolver {
5764 resolver.matches_target(path)
5765 } else {
5766 path.segments.len() == 1 && path.segments.last().unwrap().ident == self.old_name
5768 }
5769 }
5770}
5771
5772impl VisitMut for FunctionRenamer {
5773 fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
5774 self.rename_ident(&mut node.sig.ident);
5776 syn::visit_mut::visit_item_fn_mut(self, node);
5777 }
5778
5779 fn visit_impl_item_fn_mut(&mut self, node: &mut syn::ImplItemFn) {
5780 self.rename_ident(&mut node.sig.ident);
5782 syn::visit_mut::visit_impl_item_fn_mut(self, node);
5783 }
5784
5785 fn visit_trait_item_fn_mut(&mut self, node: &mut syn::TraitItemFn) {
5786 self.rename_ident(&mut node.sig.ident);
5788 syn::visit_mut::visit_trait_item_fn_mut(self, node);
5789 }
5790
5791 fn visit_expr_mut(&mut self, expr: &mut syn::Expr) {
5792 match expr {
5793 syn::Expr::Call(call) => {
5794 if let syn::Expr::Path(expr_path) = &mut *call.func {
5796 if self.matches_target_function(&expr_path.path) {
5797 if let Some(last_seg) = expr_path.path.segments.last_mut() {
5798 self.rename_ident(&mut last_seg.ident);
5799 }
5800 }
5801 }
5802 }
5803 syn::Expr::Path(expr_path) => {
5804 if self.matches_target_function(&expr_path.path) {
5806 if let Some(last_seg) = expr_path.path.segments.last_mut() {
5807 self.rename_ident(&mut last_seg.ident);
5808 }
5809 }
5810 }
5811 _ => {}
5812 }
5813 syn::visit_mut::visit_expr_mut(self, expr);
5814 }
5815}
5816
5817struct FunctionReplacementCollector {
5819 old_name: String,
5820 new_name: String,
5821 path_resolver: Option<PathResolver>,
5822 replacements: Vec<crate::surgical::Replacement>,
5823}
5824
5825impl FunctionReplacementCollector {
5826 fn collect_replacement(&mut self, ident: &syn::Ident) {
5828 if ident == &self.old_name {
5829 let span = ident.span();
5830 let start = span.start();
5831 let end = span.end();
5832
5833 self.replacements.push(crate::surgical::Replacement::new(
5834 start,
5835 end,
5836 self.new_name.clone(),
5837 ));
5838 }
5839 }
5840
5841 fn matches_target_function(&self, path: &syn::Path) -> bool {
5843 if let Some(resolver) = &self.path_resolver {
5844 resolver.matches_target(path)
5845 } else {
5846 path.segments.len() == 1 && path.segments.last().unwrap().ident == self.old_name
5848 }
5849 }
5850}
5851
5852impl<'ast> syn::visit::Visit<'ast> for FunctionReplacementCollector {
5853 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
5854 self.collect_replacement(&node.sig.ident);
5856 syn::visit::visit_item_fn(self, node);
5857 }
5858
5859 fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
5860 self.collect_replacement(&node.sig.ident);
5862 syn::visit::visit_impl_item_fn(self, node);
5863 }
5864
5865 fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
5866 self.collect_replacement(&node.sig.ident);
5868 syn::visit::visit_trait_item_fn(self, node);
5869 }
5870
5871 fn visit_expr(&mut self, expr: &'ast syn::Expr) {
5872 match expr {
5873 syn::Expr::Call(call) => {
5874 if let syn::Expr::Path(expr_path) = &*call.func {
5876 if self.matches_target_function(&expr_path.path) {
5877 if let Some(last_seg) = expr_path.path.segments.last() {
5878 self.collect_replacement(&last_seg.ident);
5879 }
5880 }
5881 }
5882 for arg in &call.args {
5884 syn::visit::visit_expr(self, arg);
5885 }
5886 return;
5888 }
5889 syn::Expr::Path(expr_path) => {
5890 if self.matches_target_function(&expr_path.path) {
5892 if let Some(last_seg) = expr_path.path.segments.last() {
5893 self.collect_replacement(&last_seg.ident);
5894 }
5895 }
5896 }
5897 _ => {}
5898 }
5899 syn::visit::visit_expr(self, expr);
5900 }
5901}
5902
5903fn generate_doc_comment(text: &str, style: &DocCommentStyle) -> String {
5909 match style {
5910 DocCommentStyle::Line => {
5911 text.lines()
5913 .map(|line| {
5914 if line.trim().is_empty() {
5915 "///".to_string()
5916 } else {
5917 format!("/// {}", line)
5918 }
5919 })
5920 .collect::<Vec<_>>()
5921 .join("\n")
5922 }
5923 DocCommentStyle::Block => {
5924 if text.contains('\n') {
5926 let lines = text.lines()
5928 .map(|line| format!(" * {}", line))
5929 .collect::<Vec<_>>()
5930 .join("\n");
5931 format!("/**\n{}\n */", lines)
5932 } else {
5933 format!("/** {} */", text)
5935 }
5936 }
5937 }
5938}
5939
5940fn extract_preceding_comment(content: &str, start_line: usize) -> Option<String> {
5943 if start_line == 0 {
5944 return None;
5945 }
5946
5947 let lines: Vec<&str> = content.lines().collect();
5948 if start_line > lines.len() {
5949 return None;
5950 }
5951
5952 let line_idx = start_line.saturating_sub(1); let mut comment_start = line_idx;
5956 let mut found_any_comment = false;
5957
5958 while comment_start > 0 {
5959 let prev_line = lines[comment_start - 1].trim();
5960
5961 let is_comment = prev_line.starts_with("///")
5963 || prev_line.starts_with("//!")
5964 || prev_line.starts_with("//")
5965 || prev_line.starts_with("/**")
5966 || prev_line.starts_with("/*!")
5967 || prev_line.starts_with("/*")
5968 || (prev_line.starts_with("*") && !prev_line.starts_with("*/"))
5969 || prev_line == "*/";
5970
5971 if is_comment {
5972 comment_start -= 1;
5973 found_any_comment = true;
5974 } else if prev_line.is_empty() && found_any_comment {
5975 comment_start -= 1;
5978 } else {
5979 break;
5980 }
5981 }
5982
5983 if !found_any_comment {
5984 return None;
5985 }
5986
5987 let comment_lines: Vec<String> = lines[comment_start..line_idx]
5989 .iter()
5990 .map(|&line| line.to_string())
5991 .collect();
5992
5993 if comment_lines.is_empty() {
5994 None
5995 } else {
5996 Some(comment_lines.join("\n"))
5997 }
5998}
5999
6000struct TargetFinder {
6002 target_type: String,
6003 target_name: String,
6004 found_position: Option<(usize, String)>, }
6006
6007impl TargetFinder {
6008 fn new(target_type: String, target_name: String) -> Self {
6009 Self {
6010 target_type,
6011 target_name,
6012 found_position: None,
6013 }
6014 }
6015}
6016
6017impl<'ast> syn::visit::Visit<'ast> for TargetFinder {
6018 fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
6019 if self.target_type == "struct" && node.ident.to_string() == self.target_name {
6020 let line = node.struct_token.span.start().line;
6023 self.found_position = Some((line, String::new()));
6024 }
6025 syn::visit::visit_item_struct(self, node);
6026 }
6027
6028 fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
6029 if self.target_type == "enum" && node.ident.to_string() == self.target_name {
6030 let line = node.enum_token.span.start().line;
6032 self.found_position = Some((line, String::new()));
6033 }
6034 syn::visit::visit_item_enum(self, node);
6035 }
6036
6037 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
6038 if self.target_type == "function" && node.sig.ident.to_string() == self.target_name {
6039 let line = node.sig.fn_token.span.start().line;
6041 self.found_position = Some((line, String::new()));
6042 }
6043 syn::visit::visit_item_fn(self, node);
6044 }
6045}
6046
6047impl RustEditor {
6048 pub fn add_doc_comment_surgical(
6050 &mut self,
6051 target_type: &str,
6052 target_name: &str,
6053 doc_text: &str,
6054 style: &DocCommentStyle,
6055 ) -> Result<ModificationResult> {
6056 use syn::visit::Visit;
6057
6058 let mut finder = TargetFinder::new(
6060 target_type.to_string(),
6061 target_name.to_string(),
6062 );
6063 finder.visit_file(&self.syntax_tree);
6064
6065 if let Some((line_num, _indent)) = finder.found_position {
6066 let line_idx = line_num.saturating_sub(1);
6068
6069 let comment = generate_doc_comment(doc_text, style);
6071
6072 let lines: Vec<&str> = self.content.lines().collect();
6074 if line_idx >= lines.len() {
6075 anyhow::bail!("Target not found at line {}", line_num);
6076 }
6077
6078 let target_line = lines[line_idx].to_string(); let target_line_len = target_line.len();
6080
6081 let indent = target_line
6083 .chars()
6084 .take_while(|c| c.is_whitespace())
6085 .collect::<String>();
6086
6087 let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
6089
6090 let comment_lines: Vec<String> = comment
6092 .lines()
6093 .map(|line| format!("{}{}", indent, line))
6094 .collect();
6095
6096 for (i, comment_line) in comment_lines.iter().rev().enumerate() {
6098 new_lines.insert(line_idx, comment_line.clone());
6099 }
6100
6101 self.content = new_lines.join("\n");
6103
6104 self.syntax_tree = syn::parse_str(&self.content)
6106 .context("Failed to re-parse after adding comment")?;
6107
6108 Ok(ModificationResult {
6109 changed: true,
6110 modified_nodes: vec![BackupNode {
6111 node_type: target_type.to_string(),
6112 identifier: target_name.to_string(),
6113 original_content: target_line,
6114 location: NodeLocation {
6115 line: line_num,
6116 column: 1,
6117 end_line: line_num,
6118 end_column: target_line_len,
6119 },
6120 }],
6121 unmatched_qualified_paths: None,
6122 })
6123 } else {
6124 anyhow::bail!("Target {} '{}' not found", target_type, target_name)
6125 }
6126 }
6127
6128 pub fn update_doc_comment_surgical(
6130 &mut self,
6131 target_type: &str,
6132 target_name: &str,
6133 doc_text: &str,
6134 style: &DocCommentStyle,
6135 ) -> Result<ModificationResult> {
6136 use syn::visit::Visit;
6137
6138 let mut finder = TargetFinder::new(
6140 target_type.to_string(),
6141 target_name.to_string(),
6142 );
6143 finder.visit_file(&self.syntax_tree);
6144
6145 if let Some((line_num, _indent)) = finder.found_position {
6146 let line_idx = line_num.saturating_sub(1);
6148
6149 let lines: Vec<&str> = self.content.lines().collect();
6151 if line_idx >= lines.len() {
6152 anyhow::bail!("Target not found at line {}", line_num);
6153 }
6154
6155 let target_line = lines[line_idx].to_string(); let target_line_len = target_line.len();
6157
6158 let indent = target_line
6160 .chars()
6161 .take_while(|c| c.is_whitespace())
6162 .collect::<String>();
6163
6164 let mut doc_comment_start = line_idx;
6166 while doc_comment_start > 0 {
6167 let prev_line = lines[doc_comment_start - 1].trim();
6168 if prev_line.starts_with("///") || prev_line.starts_with("//!") ||
6169 prev_line.starts_with("/**") || prev_line.starts_with("/*!") ||
6170 (prev_line.starts_with("*") && !prev_line.starts_with("*/")) ||
6171 prev_line == "*/" {
6172 doc_comment_start -= 1;
6173 } else {
6174 break;
6175 }
6176 }
6177
6178 let mut new_lines: Vec<String> = Vec::new();
6180
6181 for i in 0..doc_comment_start {
6183 new_lines.push(lines[i].to_string());
6184 }
6185
6186 let comment = generate_doc_comment(doc_text, style);
6188 let comment_lines: Vec<String> = comment
6189 .lines()
6190 .map(|line| format!("{}{}", indent, line))
6191 .collect();
6192
6193 for comment_line in comment_lines {
6194 new_lines.push(comment_line);
6195 }
6196
6197 for i in line_idx..lines.len() {
6199 new_lines.push(lines[i].to_string());
6200 }
6201
6202 self.content = new_lines.join("\n");
6204
6205 self.syntax_tree = syn::parse_str(&self.content)
6207 .context("Failed to re-parse after updating comment")?;
6208
6209 Ok(ModificationResult {
6210 changed: true,
6211 modified_nodes: vec![BackupNode {
6212 node_type: target_type.to_string(),
6213 identifier: target_name.to_string(),
6214 original_content: target_line,
6215 location: NodeLocation {
6216 line: line_num,
6217 column: 1,
6218 end_line: line_num,
6219 end_column: target_line_len,
6220 },
6221 }],
6222 unmatched_qualified_paths: None,
6223 })
6224 } else {
6225 anyhow::bail!("Target {} '{}' not found", target_type, target_name)
6226 }
6227 }
6228
6229 pub fn remove_doc_comment_surgical(
6231 &mut self,
6232 target_type: &str,
6233 target_name: &str,
6234 ) -> Result<ModificationResult> {
6235 use syn::visit::Visit;
6236
6237 let mut finder = TargetFinder::new(
6239 target_type.to_string(),
6240 target_name.to_string(),
6241 );
6242 finder.visit_file(&self.syntax_tree);
6243
6244 if let Some((line_num, _indent)) = finder.found_position {
6245 let line_idx = line_num.saturating_sub(1);
6247
6248 let lines: Vec<&str> = self.content.lines().collect();
6250 if line_idx >= lines.len() {
6251 anyhow::bail!("Target not found at line {}", line_num);
6252 }
6253
6254 let target_line = lines[line_idx].to_string(); let target_line_len = target_line.len();
6256
6257 let mut doc_comment_start = line_idx;
6259 while doc_comment_start > 0 {
6260 let prev_line = lines[doc_comment_start - 1].trim();
6261 if prev_line.starts_with("///") || prev_line.starts_with("//!") ||
6262 prev_line.starts_with("/**") || prev_line.starts_with("/*!") ||
6263 (prev_line.starts_with("*") && !prev_line.starts_with("*/")) ||
6264 prev_line == "*/" {
6265 doc_comment_start -= 1;
6266 } else {
6267 break;
6268 }
6269 }
6270
6271 let mut new_lines: Vec<String> = Vec::new();
6273
6274 for i in 0..doc_comment_start {
6276 new_lines.push(lines[i].to_string());
6277 }
6278
6279 for i in line_idx..lines.len() {
6283 new_lines.push(lines[i].to_string());
6284 }
6285
6286 self.content = new_lines.join("\n");
6288
6289 self.syntax_tree = syn::parse_str(&self.content)
6291 .context("Failed to re-parse after removing comment")?;
6292
6293 Ok(ModificationResult {
6294 changed: true,
6295 modified_nodes: vec![BackupNode {
6296 node_type: target_type.to_string(),
6297 identifier: target_name.to_string(),
6298 original_content: target_line,
6299 location: NodeLocation {
6300 line: line_num,
6301 column: 1,
6302 end_line: line_num,
6303 end_column: target_line_len,
6304 },
6305 }],
6306 unmatched_qualified_paths: None,
6307 })
6308 } else {
6309 anyhow::bail!("Target {} '{}' not found", target_type, target_name)
6310 }
6311 }
6312}