Skip to main content

rs_hack/
editor.rs

1//! The RustEditor is the core engine of rs-hack. It parses Rust source
2//! into a syn AST, applies refactoring operations (add/remove/rename/update),
3//! and emits modified code via surgical edits or prettyplease reformatting.
4
5use 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>, // Byte offset for each line start
22}
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    /// Format a field without extra spaces (e.g., "pub name: String" not "pub name : String")
39    fn format_field(field: &Field) -> String {
40        let mut result = String::new();
41
42        // Add visibility
43        if let syn::Visibility::Public(_) = field.vis {
44            result.push_str("pub ");
45        }
46
47        // Add field name
48        if let Some(ident) = &field.ident {
49            result.push_str(&ident.to_string());
50        }
51
52        // Add colon and type (no space before colon)
53        result.push_str(": ");
54
55        // Format type without extra spaces
56        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    /// Find similar field names using Levenshtein distance for fuzzy matching
74    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)  // Max 3 character difference
80            .collect();
81
82        scored.sort_by_key(|(_, distance)| *distance);
83        scored.into_iter()
84            .take(3)  // Top 3 suggestions
85            .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, // Default to line style for updates
118            ),
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        // Check if this is an enum variant struct literal (contains ::)
134        let is_enum_variant = op.struct_name.contains("::");
135
136        // Check if this is literal-only mode (has literal_default but field_def has no type)
137        // This allows operating on imported structs without needing the struct definition
138        let has_type = op.field_def.contains(':');
139        let is_literal_only = op.literal_default.is_some() && !has_type;
140
141        // For enum variant literals OR literal-only operations, operate on struct literals only
142        // (enum variants: can't modify definition without full enum context)
143        // (literal-only: struct might be imported, don't need definition)
144        if is_enum_variant || is_literal_only {
145            // Parse the field_def to extract field name and value/type
146            // field_def can be:
147            // - "layer: None" (with explicit value)
148            // - "layer: Option<Layer>" (with type, needs literal_default)
149            // - "layer" (just name, needs literal_default)
150
151            let final_field_def = if let Some(literal_default) = &op.literal_default {
152                // literal_default provided - use it
153                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                // field_def already contains a value (e.g., "layer: None")
160                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            // Create the AddStructLiteralFieldOp
169            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            // Update all struct literals
177            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        // Find the struct and clone it to avoid borrowing issues
184        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        // Check if the struct matches the where filter (if specified)
196        if let Some(ref where_filter) = op.where_filter {
197            if !self.matches_where_filter(&item_struct.attrs, where_filter)? {
198                // Struct doesn't match filter - skip without error
199                return Ok(ModificationResult {
200                    changed: false,
201                    modified_nodes: vec![],
202                    unmatched_qualified_paths: None,
203                });
204            }
205        }
206
207        // If literal_default is NOT provided, only modify the definition
208        if op.literal_default.is_none() {
209            // Create backup of original struct before modification
210            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            // Insert the field into the struct definition
218            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        // If literal_default IS provided:
237        // 1. Try to add to definition (idempotent - silently skips if field exists OR if field_def is incomplete)
238        // 2. Always update literals
239        let literal_default = op.literal_default.as_ref().unwrap();
240
241        // Check if field_def contains a type (has ':')
242        // If it doesn't, skip definition modification (literals-only mode)
243        let has_type = op.field_def.contains(':');
244
245        let mut def_modified = false;
246        if has_type {
247            // Create backup before any modifications
248            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            // Try to insert field into definition (idempotent - returns false if already exists)
256            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                // Re-parse the content to update syntax_tree with the struct field changes
262                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        // Always update literals when literal_default is provided
269        // Extract field name from field_def (e.g., "return_type: Option<Type>" -> "return_type" or just "return_type")
270        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        // Create the AddStructLiteralFieldOp
276        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,  // Path resolution not available from struct field operations
281        };
282
283        // Update all struct literals
284        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            // Parse the new field
298            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            // Check if field already exists
311            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                // Field already exists, skip adding
319                return Ok(false);
320            }
321            
322            // Determine insertion point
323            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                        // Empty struct, insert after the opening brace
329                        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                        // Find the comma or end
337                        self.find_after_field_end(end)
338                    } else {
339                        // Empty struct
340                        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            // Format the new field
360            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        // Check if this is an enum variant struct literal (contains ::)
377        let is_enum_variant = op.struct_name.contains("::");
378
379        // For enum variant literals, we can't update the enum variant definition
380        // (would need full enum context), so bail with helpful message
381        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        // Find the struct and clone it to avoid borrowing issues
391        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        // Check if the struct matches the where filter (if specified)
403        if let Some(ref where_filter) = op.where_filter {
404            if !self.matches_where_filter(&item_struct.attrs, where_filter)? {
405                // Struct doesn't match filter - skip without error
406                return Ok(ModificationResult {
407                    changed: false,
408                    modified_nodes: vec![],
409                    unmatched_qualified_paths: None,
410                });
411            }
412        }
413
414        // Create backup of original struct before modification
415        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            // Parse the new field definition to get the field name
434            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            // Extract the field name from the parsed field
447            let field_name = new_field.ident.as_ref()
448                .map(|i| i.to_string())
449                .context("Field must have a name")?;
450
451            // Find the existing field
452            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            // Get the span of the existing field
457            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            // Format and replace the field
461            let new_field_str = Self::format_field(&new_field);
462
463            // Remove the old field and insert the new one
464            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        // Check if this is an enum variant struct literal (contains ::)
477        let is_enum_variant = op.struct_name.contains("::");
478
479        // For enum variant literals, automatically operate on literals only
480        // (since we can't modify the enum variant definition without the full enum context)
481        let effective_literal_only = op.literal_only || is_enum_variant;
482
483        // Step 1: Remove from struct definition (unless literal_only is true or it's an enum variant)
484        if !effective_literal_only {
485            // Find the struct and clone it to avoid borrowing issues
486            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            // Check if the struct matches the where filter (if specified)
498            if let Some(ref where_filter) = op.where_filter {
499                if !self.matches_where_filter(&item_struct.attrs, where_filter)? {
500                    // Struct doesn't match filter - skip without error
501                    return Ok(ModificationResult {
502                        changed: false,
503                        modified_nodes: vec![],
504            unmatched_qualified_paths: None,
505                    });
506                }
507            }
508
509            // Create backup of original struct before modification
510            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                // Find the field to remove
519                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 not found, provide helpful error with suggestions
523                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                // Get the span including the comma
551                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                // Find and include the comma and any trailing whitespace/newline
555                while end < self.content.len() {
556                    match self.content.as_bytes()[end] as char {
557                        ',' => {
558                            end += 1;
559                            // Also consume the newline after the comma if present
560                            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                // Also need to remove leading whitespace/indentation on the same line
575                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                // Check if there's only whitespace between line_start and start
581                let before_field = &self.content[line_start..start];
582                if before_field.trim().is_empty() {
583                    // Remove the whole line
584                    self.content.replace_range(line_start..end, "");
585                } else {
586                    // Just remove the field and comma
587                    self.content.replace_range(start..end, "");
588                }
589
590                modified_nodes.push(backup_node);
591                changed = true;
592
593                // Re-parse the syntax tree after surgical edit
594                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        // Step 2: Remove from all struct literal expressions using surgical editing
601        // Collect backups of struct literals before modification
602        let literal_backups = self.collect_struct_literal_backups(&op.struct_name, None);
603
604        // Find all field deletions (surgical approach)
605        use syn::visit::Visit;
606
607        struct FieldDeletionFinder<'a> {
608            struct_name: String,
609            field_name: String,
610            deletion_ranges: Vec<(usize, usize)>, // (start_byte, end_byte)
611            unmatched_paths: std::collections::HashMap<String, usize>, // Collect qualified paths that didn't match
612            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                // Check if this matches our target
618                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                    // Simple name (no ::) - only match single-segment paths
633                    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 doesn't match but last segment equals our struct name, track as unmatched
639                    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                    // Find the field to remove
655                    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                        // Include the trailing comma if present
669                        // Look ahead for comma and optional whitespace
670                        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                                    // Also consume the newline after comma if present
676                                    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                        // Also include leading whitespace/newline on the same line
689                        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        // Prepare unmatched paths hint (only if using simple name and found unmatched paths)
720        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            // Sort ranges in reverse order to preserve offsets
728            let mut ranges = finder.deletion_ranges;
729            ranges.sort_by_key(|(start, _)| std::cmp::Reverse(*start));
730
731            // Perform surgical deletions
732            for (start, end) in ranges {
733                self.content.drain(start..end);
734            }
735
736            // Re-parse to update syntax_tree
737            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        // Parse the field name from field_def (e.g., "return_type: None" -> "return_type")
754        let field_name = op.field_def.split(':')
755            .next()
756            .map(|s| s.trim().to_string())
757            .context("Field definition must contain ':'")?;
758
759        // Create a path resolver if a canonical path was provided
760        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            // Scan the file for use statements to build the alias map
765            resolver.scan_file(&self.syntax_tree);
766            Some(resolver)
767        } else {
768            None
769        };
770
771        // Collect backups of all struct literal expressions that will be modified
772        let backup_nodes = self.collect_struct_literal_backups(&op.struct_name, path_resolver.as_ref());
773
774        // Find all struct literals and their field insertion points (surgical approach)
775        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)>, // (byte_offset, indentation_spaces)
782            unmatched_paths: std::collections::HashMap<String, usize>, // Collect qualified paths that didn't match
783            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                // Parse macro contents to find struct literals inside (e.g., vec![...])
789                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                // Check if this matches our target
804                let is_match = if let Some(resolver) = &self.path_resolver {
805                    resolver.matches_target(&node.path)
806                } else {
807                    // Legacy matching logic
808                    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                        // Simple name (no ::) - only match single-segment paths
823                        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 doesn't match but last segment equals our struct name, track as unmatched
829                        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                    // Check if field already exists
846                    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                        // Find insertion point after last field or after opening brace
852                        let insert_offset = if let Some(last_field) = node.fields.last() {
853                            // Insert after last field
854                            self.editor.span_to_byte_offset(last_field.span().end())
855                        } else {
856                            // No fields, insert after opening brace
857                            let brace_pos = self.editor.span_to_byte_offset(node.brace_token.span.join().start());
858                            brace_pos + 1 // After the '{'
859                        };
860
861                        // Determine indentation from the last field or struct context
862                        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                            // Use struct opening indentation + 4 spaces
867                            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        // Prepare unmatched paths hint (only if using simple name and found unmatched paths)
891        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        // Sort insertion points in reverse order so we can insert from end to beginning
906        // (this way earlier offsets don't get invalidated by later insertions)
907        let mut points = inserter.insertion_points;
908        points.sort_by_key(|(offset, _)| std::cmp::Reverse(*offset));
909
910        // Perform surgical insertions
911        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        // Re-parse to update syntax_tree
918        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    /// Set the base expression (..expr) on struct literals that don't already have one
930    /// e.g., `Foo { a: 1 }` → `Foo { a: 1, ..Default::default() }`
931    pub(crate) fn set_struct_literal_base(&mut self, op: &SetStructLiteralBaseOp) -> Result<ModificationResult> {
932        // Expand "default" shorthand to "Default::default()"
933        let base_expr = if op.base_expr == "default" {
934            "Default::default()".to_string()
935        } else {
936            op.base_expr.clone()
937        };
938
939        // Create a path resolver if a canonical path was provided
940        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        // Collect backups of all struct literal expressions that will be modified
950        let backup_nodes = self.collect_struct_literal_backups(&op.struct_name, path_resolver.as_ref());
951
952        // Find all struct literals that need a base expression
953        use syn::visit::Visit;
954
955        struct BaseInserter<'a> {
956            struct_name: String,
957            path_resolver: Option<&'a PathResolver>,
958            // (insert_after_offset, close_brace_offset, indent_spaces)
959            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                // Skip if already has a base expression
979                if node.rest.is_some() {
980                    syn::visit::visit_expr_struct(self, node);
981                    return;
982                }
983
984                // Check if this matches our target
985                let is_match = if let Some(resolver) = &self.path_resolver {
986                    resolver.matches_target(&node.path)
987                } else {
988                    // Legacy matching logic
989                    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                    // Find where to insert: after last field, or after opening brace if empty
1025                    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                        // No fields, insert after opening brace
1035                        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        // Sort insertion points in reverse order so we can insert from end to beginning
1073        let mut points = inserter.insertion_points;
1074        points.sort_by_key(|(insert_after, _, _)| std::cmp::Reverse(*insert_after));
1075
1076        // Perform surgical insertions
1077        for (insert_after, close_brace_offset, indent_spaces) in points {
1078            // Check if there's already a comma after the last field (syn span doesn't include trailing comma)
1079            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            // If there's a trailing comma, adjust insert point to after the comma
1084            let actual_insert_pos = if has_trailing_comma {
1085                // Find the comma position and insert after it
1086                let comma_offset = after_insert.find(',').unwrap();
1087                insert_after + comma_offset + 1
1088            } else {
1089                insert_after
1090            };
1091
1092            // Check if this is multiline (there's a newline between insert point and close brace)
1093            let between = &self.content[actual_insert_pos..close_brace_offset];
1094            let is_multiline = between.contains('\n');
1095
1096            // Also check if we're inserting right after opening brace (no fields)
1097            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                // Multiline: add on a new line with proper indentation
1103                let indent = " ".repeat(indent_spaces);
1104                if after_open_brace || has_trailing_comma {
1105                    // No comma needed - either after brace or we're after the existing comma
1106                    format!("\n{}..{}", indent, base_expr)
1107                } else {
1108                    // No trailing comma, add comma first
1109                    format!(",\n{}..{}", indent, base_expr)
1110                }
1111            } else {
1112                // Single line: add inline
1113                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        // Re-parse to update syntax_tree
1124        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    /// Collect backups of all struct literal expressions for a given struct name
1136    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                // Parse macro contents to find struct literals inside (e.g., vec![...])
1151                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                        // Use PathResolver for safe matching
1168                        resolver.matches_target(&expr_struct.path)
1169                    } else {
1170                        // Fallback to legacy pattern matching
1171                        // - "Rectangle" → only Rectangle { ... } (no :: prefix)
1172                        // - "*::Rectangle" → any path ending with Rectangle (View::Rectangle, etc.)
1173                        // - "View::Rectangle" → exact match only View::Rectangle
1174
1175                        if self.struct_name.contains("::") {
1176                            // Pattern contains :: - check for exact or wildcard match
1177                            if self.struct_name.starts_with("*::") {
1178                                // Wildcard: *::Rectangle matches any path ending with Rectangle
1179                                let target_name = &self.struct_name[3..]; // Skip "*::"
1180                                expr_struct.path.segments.last()
1181                                    .map(|seg| seg.ident.to_string() == target_name)
1182                                    .unwrap_or(false)
1183                            } else {
1184                                // Exact path match: View::Rectangle
1185                                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                            // No :: in pattern - only match pure struct literals (no path qualifier)
1193                            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                        // Extract original source text with formatting preserved
1202                        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, // We don't have precise location info in visitor
1212                                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        // Find the enum and clone it to avoid borrowing issues
1238        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        // Check if the enum matches the where filter (if specified)
1250        if let Some(ref where_filter) = op.where_filter {
1251            if !self.matches_where_filter(&item_enum.attrs, where_filter)? {
1252                // Enum doesn't match filter - skip without error
1253                return Ok(ModificationResult {
1254                    changed: false,
1255                    modified_nodes: vec![],
1256                    unmatched_qualified_paths: None,
1257                });
1258            }
1259        }
1260
1261        // Create backup of original enum before modification
1262        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        // Parse the new variant
1280        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        // Check if variant already exists
1289        let variant_name = new_variant.ident.to_string();
1290        if item_enum.variants.iter().any(|v| v.ident.to_string() == variant_name) {
1291            // Variant already exists, skip adding
1292            return Ok(false);
1293        }
1294
1295        // Determine insertion point
1296        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        // Find the enum and clone it
1339        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        // Check if the enum matches the where filter (if specified)
1351        if let Some(ref where_filter) = op.where_filter {
1352            if !self.matches_where_filter(&item_enum.attrs, where_filter)? {
1353                // Enum doesn't match filter - skip without error
1354                return Ok(ModificationResult {
1355                    changed: false,
1356                    modified_nodes: vec![],
1357                    unmatched_qualified_paths: None,
1358                });
1359            }
1360        }
1361
1362        // Create backup of original enum before modification
1363        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        // Parse the new variant to get its name
1371        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        // Find the existing variant
1382        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        // Get the span
1387        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        // Format and replace
1391        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        // Find the enum
1403        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        // Check if the enum matches the where filter (if specified)
1415        if let Some(ref where_filter) = op.where_filter {
1416            if !self.matches_where_filter(&item_enum.attrs, where_filter)? {
1417                // Enum doesn't match filter - skip without error
1418                return Ok(ModificationResult {
1419                    changed: false,
1420                    modified_nodes: vec![],
1421                    unmatched_qualified_paths: None,
1422                });
1423            }
1424        }
1425
1426        // Create backup of original enum before modification
1427        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        // Find the variant to remove
1435        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        // Get the span including comma
1440        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        // Find and include the comma and trailing whitespace
1444        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        // Remove leading whitespace on the line
1463        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            // Auto-detect mode: find all missing enum variants
1485            self.add_missing_match_arms(op)
1486        } else {
1487            // Normal mode: add a single match arm
1488            self.add_single_match_arm(op)
1489        }
1490    }
1491
1492    fn add_single_match_arm(&mut self, op: &AddMatchArmOp) -> Result<ModificationResult> {
1493        // Parse the pattern and body by creating a dummy match expression
1494        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        // Extract the arm from the dummy match
1499        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        // Collect backup of function before modification
1507        let backup_node = if let Some(ref fn_name) = op.function_name {
1508            self.get_function_backup(fn_name)?
1509        } else {
1510            // If no function specified, we'll backup all modified functions later
1511            // For now, create a generic backup
1512            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        // Find and modify match expressions
1526        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            // Replace just the modified function
1538            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    /// Format a single item to string using prettyplease
1554    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    /// Isolated prettyplease: Reformat only a specific item, preserving the rest of the file
1564    /// This finds an item in the original syntax tree, formats it with prettyplease, and surgically replaces it
1565    fn reformat_item_isolated<F>(&mut self, predicate: F) -> Result<bool>
1566    where
1567        F: Fn(&Item) -> bool,
1568    {
1569        // Find the item index in the ORIGINAL syntax tree (before mutations)
1570        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        // Get the byte range of the original item
1579        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        // Get the modified item from the current syntax tree
1583        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        // Format just this item with prettyplease
1589        let formatted_item = self.unparse_item(modified_item);
1590
1591        // Surgically replace the old item with the formatted version
1592        self.content.replace_range(start..end, &formatted_item);
1593
1594        // Re-parse to update syntax_tree
1595        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    /// Get backup of a function before modification
1603    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        // Get the enum name
1621        let enum_name = op.enum_name.as_ref()
1622            .ok_or_else(|| anyhow::anyhow!("enum_name is required for auto-detect"))?;
1623
1624        // Find all enum variants
1625        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        // Find existing match arms
1632        let existing_patterns = self.find_existing_match_patterns(&op.function_name);
1633
1634        // Determine missing variants
1635        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        // Get backup of function before modification
1659        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        // Add ALL missing match arms in one pass using a visitor
1676        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        // Find and modify match expressions with all arms at once
1691        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            // Print what was added
1703            for variant in &missing_variants {
1704                println!("Added match arm for: {}::{}", enum_name, variant);
1705            }
1706
1707            // Replace just the modified function
1708            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        // Find the enum in the syntax tree
1725        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                // Check if we're in the right function (if specified)
1758                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                // Collect all patterns
1766                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        // Get backup of function before modification
1786        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        // Parse the new body
1803        let new_body: syn::Expr = parse_str(&op.new_body)
1804            .with_context(|| format!("Failed to parse new body: {}", op.new_body))?;
1805
1806        // Find and modify match expressions
1807        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            // Replace just the modified function
1820            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        // Get backup of function before modification
1833        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        // Find and modify match expressions
1850        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            // Replace just the modified function
1862            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        // Parse the method definition
1875        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        // Get the method name for idempotency check
1884        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        // Find the impl block
1890        let impl_index = self.syntax_tree.items.iter().position(|item| {
1891            if let Item::Impl(impl_block) = item {
1892                // Check if this is the right impl block
1893                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        // Check if method already exists (idempotent)
1903        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        // Create backup of original impl block before modification
1925        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        // Get the span before modification
1933        let impl_span = impl_block.span();
1934
1935        // Add the method to the impl block
1936        match &mut self.syntax_tree.items[impl_index] {
1937            Item::Impl(impl_block) => {
1938                // Add based on position
1939                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        // Use prettyplease to format just this impl block
1972        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        // Parse the use statement
1983        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        // Check if this use statement already exists (idempotent)
1988        let use_exists = self.syntax_tree.items.iter().any(|item| {
1989            if let Item::Use(existing_use) = item {
1990                // Compare the use trees
1991                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        // Create a simple backup for use statements (track by line position)
2007        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        // Find the position to insert the use statement
2020        let insert_index = match &op.position {
2021            InsertPosition::First => 0,
2022            InsertPosition::Last => {
2023                // Find the last use statement
2024                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                // Find the use statement matching the path
2031                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                // Find the use statement matching the path
2042                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        // Insert the use statement into the AST
2053        self.syntax_tree.items.insert(insert_index, Item::Use(use_item));
2054
2055        // Find the byte position in the source where we need to insert
2056        // We want to insert at the beginning of a line
2057        let insert_line_pos = if insert_index == 0 {
2058            // Insert at very beginning
2059            0
2060        } else {
2061            // Insert after the previous item
2062            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            // Find the end of this line (where the newline is)
2067            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            // Move past the newline to the start of the next line
2072            if line_end < self.content.len() {
2073                line_end + 1
2074            } else {
2075                // At end of file, add a newline first
2076                self.content.push('\n');
2077                self.content.len()
2078            }
2079        };
2080
2081        // Format the use statement
2082        let use_str = format!("use {};\n", op.use_path);
2083
2084        // Insert the use statement
2085        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        // Find the target item (struct or enum)
2096        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        // Get the item and check for existing derives
2105        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        // Check if the item matches the where filter (if specified)
2112        if let Some(ref where_filter) = op.where_filter {
2113            if !self.matches_where_filter(item_attrs, where_filter)? {
2114                // Item doesn't match filter - skip without error
2115                return Ok(ModificationResult {
2116                    changed: false,
2117                    modified_nodes: vec![],
2118                    unmatched_qualified_paths: None,
2119                });
2120            }
2121        }
2122
2123        // Create backup of original item before modification
2124        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        // Filter out derives that already exist (idempotent)
2132        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            // All derives already exist
2139            return Ok(ModificationResult {
2140                changed: false,
2141                modified_nodes: vec![],
2142                unmatched_qualified_paths: None,
2143            });
2144        }
2145
2146        // Combine existing and new derives
2147        let mut all_derives = existing_derives;
2148        all_derives.extend(new_derives);
2149
2150        // Convert to string refs for the update function
2151        let all_derives_refs: Vec<&str> = all_derives.iter().map(|s| s.as_str()).collect();
2152
2153        // Update the AST item's attributes
2154        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        // Use prettyplease to format just this item
2165        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    /// Replace an item in the content with a formatted version
2175    fn replace_formatted_item(&mut self, item_index: usize, original_span: Span) -> Result<()> {
2176        // Get the item start and end positions from the original source
2177        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        // Find the actual start (including attributes)
2181        let mut actual_start = item_start_pos;
2182
2183        // Search backwards for attributes
2184        let mut temp_pos = item_start_pos;
2185        while temp_pos > 0 {
2186            // Move to previous line
2187            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        // Create a temporary file with just this item for pretty formatting
2215        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        // Format the item using prettyplease
2223        let formatted = prettyplease::unparse(&temp_file);
2224        let formatted = formatted.trim();
2225
2226        // Replace in content
2227        self.content.replace_range(actual_start..item_end_pos, formatted);
2228
2229        Ok(())
2230    }
2231
2232    /// Extract existing derive traits from attributes - returns owned Strings
2233    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    /// Check if an item matches the where filter criteria
2249    /// Supports filters like:
2250    /// - "derives_trait:Clone" - matches if item derives Clone
2251    /// - "derives_trait:Clone,Debug" - matches if item derives Clone OR Debug
2252    fn matches_where_filter(&self, attrs: &[syn::Attribute], where_filter: &str) -> Result<bool> {
2253        // Parse the filter: "derives_trait:Clone,Debug"
2254        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            // Check if ANY of the required traits are present
2259            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        // Unknown filter type - default to match (don't break existing behavior)
2268        Ok(true)
2269    }
2270
2271    /// Update or create derive attribute in the attribute list
2272    fn update_derive_attr(attrs: &mut Vec<syn::Attribute>, derives: &[&str]) -> Result<()> {
2273        let derive_str = derives.join(", ");
2274
2275        // Parse a dummy struct with the derive to extract the attribute
2276        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        // Find existing derive attribute and replace it
2285        if let Some(pos) = attrs.iter().position(|a| a.path().is_ident("derive")) {
2286            attrs[pos] = new_attr;
2287        } else {
2288            // Add new derive attribute at the beginning
2289            attrs.insert(0, new_attr);
2290        }
2291
2292        Ok(())
2293    }
2294
2295    /// Replace the modified function(s) in the content with formatted versions
2296    fn replace_modified_functions(&mut self, modified_function: &Option<String>) -> Result<()> {
2297        // If no specific function was targeted, use isolated prettyplease for all modified functions
2298        if modified_function.is_none() {
2299            // This case shouldn't happen in practice, but if it does, fall back to whole-file format
2300            // TODO: Track which functions were modified and format only those
2301            self.content = prettyplease::unparse(&self.syntax_tree);
2302            return Ok(());
2303        }
2304
2305        // Parse the ORIGINAL content to get the correct spans
2306        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        // Find the function in the ORIGINAL syntax tree to get correct byte positions
2312        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        // Get the span of the original function (these are the correct byte positions)
2324        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        // Find the MODIFIED function in the modified syntax tree
2328        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        // Format just the modified function using prettyplease
2340        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        // Extract just the function (remove any extra newlines at start/end)
2349        let formatted_fn = formatted_fn.trim();
2350
2351        // Replace the function in the original content using original spans
2352        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        // Look for comma or newline after the field
2368        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        // Find the start of the current line
2381        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        // Count spaces/tabs at the start of the line
2387        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 we're inserting in an empty struct/enum, add default indentation
2400        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    /// Get a reference to the syntax tree (for revert operations)
2412    pub fn get_syntax_tree(&self) -> &syn::File {
2413        &self.syntax_tree
2414    }
2415
2416    /// Replace a range of bytes with new content (for revert operations)
2417    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        // Re-parse the syntax tree
2425        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    /// Find all locations where a field appears in the codebase
2433    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        // Search struct definitions
2440        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            // Search enum variants
2466            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        // Search struct literals
2495        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                    // Check if this struct literal has the field
2504                    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    /// Inspect and list AST nodes (e.g., struct literals) in the file
2540    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, search all node types
2547        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(); // Safe because we checked is_none above
2560
2561        match node_type {
2562            "struct-literal" => {
2563                // Find all struct literal expressions
2564                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                        // Try to parse macro contents as expressions to find struct literals inside
2574                        // This handles cases like: vec![View::Rectangle { ... }]
2575                        use syn::parse::Parser;
2576
2577                        // Try to parse the macro tokens as a punctuated list of expressions (common in vec!, etc.)
2578                        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                        // Continue with default visit behavior
2587                        syn::visit::visit_expr_macro(self, node);
2588                    }
2589
2590                    fn visit_expr_struct(&mut self, node: &'ast syn::ExprStruct) {
2591                        // Match based on pattern:
2592                        // - "Rectangle" → only Rectangle { ... } (no :: prefix)
2593                        // - "*::Rectangle" → any path ending with Rectangle (View::Rectangle, etc.)
2594                        // - "View::Rectangle" → exact match only View::Rectangle
2595
2596                        let filter = match self.name_filter {
2597                            Some(f) => f,
2598                            None => {
2599                                // No filter - match anything
2600                                // If path has multiple segments, include the full qualified path
2601                                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                        // Extract preceding comment if requested
2616                        let preceding_comment = if self.include_comments {
2617                            extract_preceding_comment(&self.editor.content, location.line)
2618                        } else {
2619                            None
2620                        };
2621
2622                                // Extract preceding comment if requested
2623                                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                        // Check if this struct literal matches the filter pattern
2644                        let matches = if filter.contains("::") {
2645                            // Pattern contains :: - check for exact or wildcard match
2646                            if filter.starts_with("*::") {
2647                                // Wildcard: *::Rectangle matches any path ending with Rectangle
2648                                let target_name = &filter[3..]; // Skip "*::"
2649                                node.path.segments.last()
2650                                    .map(|seg| seg.ident.to_string() == target_name)
2651                                    .unwrap_or(false)
2652                            } else {
2653                                // Exact path match: View::Rectangle
2654                                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                            // No :: in pattern - eagerly match any path ending with this name
2662                            // This matches both "Rectangle" and "View::Rectangle" when searching for "Rectangle"
2663                            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                        // Get the struct name for the identifier
2674                        // If path has multiple segments, include the full qualified path
2675                        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                        // Format the struct literal
2687                        let snippet = self.editor.format_expr_struct(node);
2688                        let location = self.editor.span_to_location(node.span());
2689
2690                        // Extract preceding comment if requested
2691                        let preceding_comment = if self.include_comments {
2692                            extract_preceding_comment(&self.editor.content, location.line)
2693                        } else {
2694                            None
2695                        };
2696
2697                        // Extract preceding comment if requested
2698                        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(), // Will be filled in by caller
2706                            node_type: "struct-literal".to_string(),
2707                            identifier: struct_name,
2708                            location,
2709                            snippet,
2710                            preceding_comment,
2711                        });
2712
2713                        // Continue visiting nested expressions
2714                        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                // Visit all items in the file
2726                for item in &self.syntax_tree.items {
2727                    syn::visit::visit_item(&mut visitor, item);
2728                }
2729            }
2730            "match-arm" => {
2731                // Find all match arms
2732                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                        // Iterate through all arms in this match expression
2742                        for arm in &node.arms {
2743                            // Convert pattern to string for matching
2744                            let pat = &arm.pat;
2745                            let pattern_str = quote::quote!(#pat).to_string();
2746
2747                            // Apply pattern filter if specified
2748                            if let Some(filter) = self.pattern_filter {
2749                                // Normalize both for comparison (remove spaces)
2750                                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                            // Format the match arm (pattern => body)
2759                            let snippet = self.editor.format_match_arm(arm);
2760                            let location = self.editor.span_to_location(arm.span());
2761
2762                        // Extract preceding comment if requested
2763                        let preceding_comment = if self.include_comments {
2764                            extract_preceding_comment(&self.editor.content, location.line)
2765                        } else {
2766                            None
2767                        };
2768
2769                            // Extract preceding comment if requested
2770                            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(), // Will be filled in by caller
2778                                node_type: "match-arm".to_string(),
2779                                identifier: pattern_str.replace(" ", ""),
2780                                location,
2781                                snippet,
2782                                preceding_comment,
2783                            });
2784                        }
2785
2786                        // Continue visiting nested expressions
2787                        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                // Visit all items in the file
2799                for item in &self.syntax_tree.items {
2800                    syn::visit::visit_item(&mut visitor, item);
2801                }
2802            }
2803            "enum-usage" => {
2804                // Find all enum variant usages (paths like Operator::Error)
2805                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                        // Convert path to string
2815                        let path = &node.path;
2816                        let path_str = quote::quote!(#path).to_string();
2817
2818                        // Apply path filter if specified
2819                        if let Some(filter) = self.path_filter {
2820                            // Normalize both for comparison (remove spaces)
2821                            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                        // Format the path expression
2831                        let snippet = self.editor.format_expr_path(node);
2832                        let location = self.editor.span_to_location(node.span());
2833
2834                        // Extract preceding comment if requested
2835                        let preceding_comment = if self.include_comments {
2836                            extract_preceding_comment(&self.editor.content, location.line)
2837                        } else {
2838                            None
2839                        };
2840
2841                        // Extract preceding comment if requested
2842                        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(), // Will be filled in by caller
2850                            node_type: "enum-usage".to_string(),
2851                            identifier: path_str.replace(" ", ""),
2852                            location,
2853                            snippet,
2854                            preceding_comment,
2855                        });
2856
2857                        // Continue visiting nested expressions
2858                        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                // Visit all items in the file
2870                for item in &self.syntax_tree.items {
2871                    syn::visit::visit_item(&mut visitor, item);
2872                }
2873            }
2874            "function-call" => {
2875                // Find all function call expressions
2876                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                        // Extract function name from the call expression
2886                        let func_name = if let syn::Expr::Path(expr_path) = &*node.func {
2887                            // Get the last segment of the path as the function name
2888                            expr_path.path.segments.last()
2889                                .map(|seg| seg.ident.to_string())
2890                                .unwrap_or_default()
2891                        } else {
2892                            // For other expression types, use quote to convert to string
2893                            quote::quote!(#node.func).to_string()
2894                        };
2895
2896                        // Apply name filter if specified
2897                        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                        // Format the function call
2905                        let snippet = self.editor.format_expr_call(node);
2906                        let location = self.editor.span_to_location(node.span());
2907
2908                        // Extract preceding comment if requested
2909                        let preceding_comment = if self.include_comments {
2910                            extract_preceding_comment(&self.editor.content, location.line)
2911                        } else {
2912                            None
2913                        };
2914
2915                        // Extract preceding comment if requested
2916                        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(), // Will be filled in by caller
2924                            node_type: "function-call".to_string(),
2925                            identifier: func_name,
2926                            location,
2927                            snippet,
2928                            preceding_comment,
2929                        });
2930
2931                        // Continue visiting nested expressions
2932                        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                // Visit all items in the file
2944                for item in &self.syntax_tree.items {
2945                    syn::visit::visit_item(&mut visitor, item);
2946                }
2947            }
2948            "trait-method" => {
2949                // Find methods within trait definitions
2950                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                        // Track which trait this is
2961                        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                        // Create identifier with trait context if available
2973                        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                        // Apply name filter if specified
2980                        if let Some(filter) = self.name_filter {
2981                            // Support filtering by "Trait::method" or just "method"
2982                            if !identifier.contains(filter) && method_name != filter {
2983                                syn::visit::visit_trait_item_fn(self, node);
2984                                return;
2985                            }
2986                        }
2987
2988                        // Format the method definition
2989                        let snippet = self.editor.format_trait_item_fn(node);
2990                        let location = self.editor.span_to_location(node.span());
2991
2992                        // Extract preceding comment if requested
2993                        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                // Find all method call expressions
3026                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                        // Extract method name
3036                        let method_name = node.method.to_string();
3037
3038                        // Apply name filter if specified
3039                        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                        // Format the method call
3047                        let snippet = self.editor.format_expr_method_call(node);
3048                        let location = self.editor.span_to_location(node.span());
3049
3050                        // Extract preceding comment if requested
3051                        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(), // Will be filled in by caller
3059                            node_type: "method-call".to_string(),
3060                            identifier: method_name,
3061                            location,
3062                            snippet,
3063                            preceding_comment,
3064                        });
3065
3066                        // Continue visiting nested expressions
3067                        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                // Visit all items in the file
3079                for item in &self.syntax_tree.items {
3080                    syn::visit::visit_item(&mut visitor, item);
3081                }
3082            }
3083            "identifier" => {
3084                // Find all identifier references
3085                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                        // Extract identifier name
3095                        let ident_name = node.to_string();
3096
3097                        // Apply name filter if specified
3098                        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                        // Format the identifier
3106                        let snippet = self.editor.format_ident(node);
3107                        let location = self.editor.span_to_location(node.span());
3108
3109                        // Extract preceding comment if requested
3110                        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(), // Will be filled in by caller
3118                            node_type: "identifier".to_string(),
3119                            identifier: ident_name,
3120                            location,
3121                            snippet,
3122                            preceding_comment,
3123                        });
3124
3125                        // Continue visiting
3126                        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                // Visit all items in the file
3138                for item in &self.syntax_tree.items {
3139                    syn::visit::visit_item(&mut visitor, item);
3140                }
3141            }
3142            "type-ref" => {
3143                // Find all type path usages
3144                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                        // Extract type name (last segment of path)
3154                        let type_name = node.path.segments.last()
3155                            .map(|seg| seg.ident.to_string())
3156                            .unwrap_or_default();
3157
3158                        // Apply name filter if specified
3159                        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                        // Format the type path
3167                        let snippet = self.editor.format_type_path(node);
3168                        let location = self.editor.span_to_location(node.span());
3169
3170                        // Extract preceding comment if requested
3171                        let preceding_comment = if self.include_comments {
3172                            extract_preceding_comment(&self.editor.content, location.line)
3173                        } else {
3174                            None
3175                        };
3176
3177                        // Get full path for identifier
3178                        let path = &node.path;
3179                        let path_str = quote::quote!(#path).to_string();
3180
3181                        self.results.push(InspectResult {
3182                            file_path: String::new(), // Will be filled in by caller
3183                            node_type: "type-ref".to_string(),
3184                            identifier: path_str.replace(" ", ""),
3185                            location,
3186                            snippet,
3187                            preceding_comment,
3188                        });
3189
3190                        // Continue visiting
3191                        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                // Visit all items in the file
3203                for item in &self.syntax_tree.items {
3204                    syn::visit::visit_item(&mut visitor, item);
3205                }
3206            }
3207            "macro-call" => {
3208                // Find all macro call expressions
3209                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                        // Extract macro name from the path
3219                        let macro_name = node.mac.path.segments.last()
3220                            .map(|seg| seg.ident.to_string())
3221                            .unwrap_or_default();
3222
3223                        // Apply name filter if specified
3224                        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                        // Format the macro call
3232                        let snippet = self.editor.format_expr_macro(node);
3233                        let location = self.editor.span_to_location(node.span());
3234
3235                        // Extract preceding comment if requested
3236                        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(), // Will be filled in by caller
3244                            node_type: "macro-call".to_string(),
3245                            identifier: macro_name,
3246                            location,
3247                            snippet,
3248                            preceding_comment,
3249                        });
3250
3251                        // Continue visiting nested expressions
3252                        syn::visit::visit_expr_macro(self, node);
3253                    }
3254
3255                    fn visit_stmt(&mut self, node: &'ast syn::Stmt) {
3256                        // Also catch macro calls at statement level (e.g., println! as statement)
3257                        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                            // Apply name filter if specified
3263                            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                            // Format the macro call
3271                            let snippet = self.editor.format_stmt_macro(macro_stmt);
3272                            let location = self.editor.span_to_location(macro_stmt.span());
3273
3274                        // Extract preceding comment if requested
3275                        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(), // Will be filled in by caller
3283                                node_type: "macro-call".to_string(),
3284                                identifier: macro_name,
3285                                location,
3286                                snippet,
3287                                preceding_comment,
3288                            });
3289                        }
3290
3291                        // Continue visiting
3292                        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                // Visit all items in the file
3304                for item in &self.syntax_tree.items {
3305                    syn::visit::visit_item(&mut visitor, item);
3306                }
3307            }
3308            "struct" => {
3309                // Find all struct definitions
3310                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                        // Apply name filter if specified
3322                        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                        // Format the struct definition
3330                        let snippet = self.editor.format_item_struct(node);
3331                        let location = self.editor.span_to_location(node.span());
3332
3333                        // Extract preceding comment if requested
3334                        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                // Find all enum definitions
3366                // Supports:
3367                // 1. --variant Rectangle (all enums with Rectangle variant)
3368                // 2. --name View --variant Rectangle (View enum, Rectangle variant only)
3369                // 3. --name View::Rectangle (:: syntax auto-detects variant filter)
3370                // 4. --name *::Rectangle (wildcard: any enum with Rectangle variant)
3371                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                        // Parse name_filter for :: syntax
3384                        let (enum_name_filter, implicit_variant_filter) = if let Some(filter) = self.name_filter {
3385                            if filter.contains("::") {
3386                                // View::Rectangle or *::Rectangle
3387                                let parts: Vec<&str> = filter.split("::").collect();
3388                                if parts.len() == 2 {
3389                                    if parts[0] == "*" {
3390                                        // *::Rectangle - match any enum with this variant
3391                                        (None, Some(parts[1]))
3392                                    } else {
3393                                        // View::Rectangle - match specific enum with this variant
3394                                        (Some(parts[0]), Some(parts[1]))
3395                                    }
3396                                } else {
3397                                    // Invalid :: syntax, treat as exact enum name
3398                                    (Some(filter), None)
3399                                }
3400                            } else {
3401                                // Simple enum name filter
3402                                (Some(filter), None)
3403                            }
3404                        } else {
3405                            (None, None)
3406                        };
3407
3408                        // Determine which variant filter to use (explicit --variant flag or implicit from ::)
3409                        let effective_variant_filter = self.variant_filter.or(implicit_variant_filter);
3410
3411                        // Apply enum name filter if specified
3412                        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 variant filter is specified, check if this enum has that variant
3420                        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                        // Format the enum definition
3429                        let snippet = if let Some(variant_name) = effective_variant_filter {
3430                            // Filter to show only the matching variant
3431                            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                        // Extract preceding comment if requested
3439                        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                // Find all function definitions
3472                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                        // Apply name filter if specified
3484                        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                        // Format the function definition
3492                        let snippet = self.editor.format_item_fn(node);
3493                        let location = self.editor.span_to_location(node.span());
3494
3495                        // Extract preceding comment if requested
3496                        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                // Find methods within impl blocks
3528                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                        // Track which type this impl is for
3539                        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                        // Create identifier with impl type context if available
3558                        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                        // Apply name filter if specified
3565                        if let Some(filter) = self.name_filter {
3566                            // Support filtering by "Type::method" or just "method"
3567                            if !identifier.contains(filter) && method_name != filter {
3568                                syn::visit::visit_impl_item_fn(self, node);
3569                                return;
3570                            }
3571                        }
3572
3573                        // Format the method definition
3574                        let snippet = self.editor.format_impl_item_fn(node);
3575                        let location = self.editor.span_to_location(node.span());
3576
3577                        // Extract preceding comment if requested
3578                        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                // Find all trait definitions
3611                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                        // Apply name filter if specified
3623                        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                        // Format the trait definition
3631                        let snippet = self.editor.format_item_trait(node);
3632                        let location = self.editor.span_to_location(node.span());
3633
3634                        // Extract preceding comment if requested
3635                        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                // Find all const definitions
3667                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                        // Apply name filter if specified
3679                        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                        // Format the const definition
3687                        let snippet = self.editor.format_item_const(node);
3688                        let location = self.editor.span_to_location(node.span());
3689
3690                        // Extract preceding comment if requested
3691                        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                // Find all static definitions
3723                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                        // Apply name filter if specified
3735                        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                        // Format the static definition
3743                        let snippet = self.editor.format_item_static(node);
3744                        let location = self.editor.span_to_location(node.span());
3745
3746                        // Extract preceding comment if requested
3747                        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                // Find all type alias definitions
3779                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                        // Apply name filter if specified
3791                        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                        // Format the type alias definition
3799                        let snippet = self.editor.format_item_type(node);
3800                        let location = self.editor.span_to_location(node.span());
3801
3802                        // Extract preceding comment if requested
3803                        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                // Find all module definitions
3835                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                        // Apply name filter if specified
3847                        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                        // Format the module definition
3855                        let snippet = self.editor.format_item_mod(node);
3856                        let location = self.editor.span_to_location(node.span());
3857
3858                        // Extract preceding comment if requested
3859                        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                // Find all trait impl blocks (impl Trait for Type)
3891                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                        // Only emit when there's a trait clause
3900                        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                        // Apply name filter to the trait name (same wildcard semantics as struct-literal)
3911                        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                        // Extract the implementing type name
3937                        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    /// Format an ExprStruct node as a string - extracts original source
3980    fn format_expr_struct(&self, expr: &syn::ExprStruct) -> String {
3981        // Extract the original source code from the file content using the span
3982        let start = self.span_to_byte_offset(expr.span().start());
3983        let end = self.span_to_byte_offset(expr.span().end());
3984
3985        // Get the original text and collapse to single line
3986        let original = &self.content[start..end];
3987
3988        // Replace multiple whitespace/newlines with single space for single-line format
3989        original.split_whitespace().collect::<Vec<_>>().join(" ")
3990    }
3991
3992    /// Format a match arm as a string - extracts original source
3993    fn format_match_arm(&self, arm: &syn::Arm) -> String {
3994        // Extract the original source code from the file content using the span
3995        let start = self.span_to_byte_offset(arm.span().start());
3996        let end = self.span_to_byte_offset(arm.span().end());
3997
3998        // Get the original text and collapse to single line
3999        let original = &self.content[start..end];
4000
4001        // Replace multiple whitespace/newlines with single space for single-line format
4002        original.split_whitespace().collect::<Vec<_>>().join(" ")
4003    }
4004
4005    /// Format an ExprPath node as a string - extracts original source
4006    fn format_expr_path(&self, expr: &syn::ExprPath) -> String {
4007        // Extract the original source code from the file content using the span
4008        let start = self.span_to_byte_offset(expr.span().start());
4009        let end = self.span_to_byte_offset(expr.span().end());
4010
4011        // Get the original text and collapse to single line
4012        let original = &self.content[start..end];
4013
4014        // Replace multiple whitespace/newlines with single space for single-line format
4015        original.split_whitespace().collect::<Vec<_>>().join(" ")
4016    }
4017
4018    /// Format an ExprCall node as a string - extracts original source
4019    fn format_expr_call(&self, expr: &syn::ExprCall) -> String {
4020        // Extract the original source code from the file content using the span
4021        let start = self.span_to_byte_offset(expr.span().start());
4022        let end = self.span_to_byte_offset(expr.span().end());
4023
4024        // Get the original text and collapse to single line
4025        let original = &self.content[start..end];
4026
4027        // Replace multiple whitespace/newlines with single space for single-line format
4028        original.split_whitespace().collect::<Vec<_>>().join(" ")
4029    }
4030
4031    /// Format an ExprMethodCall node as a string - extracts original source
4032    fn format_expr_method_call(&self, expr: &syn::ExprMethodCall) -> String {
4033        // Extract the original source code from the file content using the span
4034        let start = self.span_to_byte_offset(expr.span().start());
4035        let end = self.span_to_byte_offset(expr.span().end());
4036
4037        // Get the original text and collapse to single line
4038        let original = &self.content[start..end];
4039
4040        // Replace multiple whitespace/newlines with single space for single-line format
4041        original.split_whitespace().collect::<Vec<_>>().join(" ")
4042    }
4043
4044    /// Format an Ident node as a string - just return the identifier
4045    fn format_ident(&self, ident: &syn::Ident) -> String {
4046        ident.to_string()
4047    }
4048
4049    /// Format a TypePath node as a string - extracts original source
4050    fn format_type_path(&self, ty: &syn::TypePath) -> String {
4051        // Extract the original source code from the file content using the span
4052        let start = self.span_to_byte_offset(ty.span().start());
4053        let end = self.span_to_byte_offset(ty.span().end());
4054
4055        // Get the original text and collapse to single line
4056        let original = &self.content[start..end];
4057
4058        // Replace multiple whitespace/newlines with single space for single-line format
4059        original.split_whitespace().collect::<Vec<_>>().join(" ")
4060    }
4061
4062    /// Format an ExprMacro node as a string - extracts original source
4063    fn format_expr_macro(&self, expr: &syn::ExprMacro) -> String {
4064        // Extract the original source code from the file content using the span
4065        let start = self.span_to_byte_offset(expr.span().start());
4066        let end = self.span_to_byte_offset(expr.span().end());
4067
4068        // Get the original text and collapse to single line
4069        let original = &self.content[start..end];
4070
4071        // Replace multiple whitespace/newlines with single space for single-line format
4072        original.split_whitespace().collect::<Vec<_>>().join(" ")
4073    }
4074
4075    /// Format a StmtMacro node as a string - extracts original source
4076    fn format_stmt_macro(&self, stmt: &syn::StmtMacro) -> String {
4077        // Extract the original source code from the file content using the span
4078        let start = self.span_to_byte_offset(stmt.span().start());
4079        let end = self.span_to_byte_offset(stmt.span().end());
4080
4081        // Get the original text and collapse to single line
4082        let original = &self.content[start..end];
4083
4084        // Replace multiple whitespace/newlines with single space for single-line format
4085        original.split_whitespace().collect::<Vec<_>>().join(" ")
4086    }
4087
4088    /// Format an ItemStruct node as a string - extracts original source
4089    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    /// Format an ItemEnum node as a string - extracts original source
4097    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    /// Format an ItemEnum showing only a specific variant
4105    fn format_item_enum_variant_only(&self, item: &syn::ItemEnum, variant_name: &str) -> String {
4106        // Find the matching variant
4107        let variant = item.variants.iter()
4108            .find(|v| v.ident.to_string() == variant_name);
4109
4110        if let Some(variant) = variant {
4111            // Extract the enum header (visibility, enum keyword, name, generics, where clause)
4112            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            // Get everything before the first variant (header)
4120            let header = &self.content[enum_start..variants_start].trim_end();
4121
4122            // Extract the specific variant source
4123            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            // Build the filtered output
4128            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            // Fallback: show full enum if variant not found
4136            self.format_item_enum(item)
4137        }
4138    }
4139
4140    /// Format an ItemFn node as a string - extracts original source
4141    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    /// Format an ImplItemFn node as a string - extracts original source
4149    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    /// Format a TraitItemFn node as a string - extracts original source
4157    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    /// Format an ItemTrait node as a string - extracts original source
4165    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    /// Format an ItemConst node as a string - extracts original source
4173    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    /// Format an ItemStatic node as a string - extracts original source
4181    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    /// Format an ItemType node as a string - extracts original source
4189    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    /// Format an ItemMod node as a string - extracts original source
4197    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    /// Find the index of an item by type and name
4205    #[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                    // For impl blocks, match on the self_ty
4220                    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    /// Replace an item at a specific index with a new item
4236    #[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        // Replace the item in the syntax tree
4243        self.syntax_tree.items[index] = new_item;
4244
4245        // Reformat the entire file using prettyplease
4246        self.content = prettyplease::unparse(&self.syntax_tree);
4247
4248        // Recompute line offsets
4249        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    /// Generic transform operation - find matching nodes and apply action
4267    pub(crate) fn transform(&mut self, op: &crate::operations::TransformOp) -> Result<ModificationResult> {
4268        use crate::operations::{InspectResult, TransformAction};
4269
4270        // First, use inspect to find all matching nodes (comments not needed for transform)
4271        let matches = self.inspect(Some(&op.node_type), op.name_filter.as_deref(), None, false)?;
4272
4273        // Apply content filter if specified
4274        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        // Now apply the transformation action to each match
4291        // We need to work backwards through the file to avoid offset issues
4292        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            // Create backup node
4302            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            // Find the byte offsets for this node
4310            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            // Extract the original text
4320            let original_text = &self.content[start_offset..end_offset];
4321
4322            // Apply the action
4323            let replacement = match &op.action {
4324                TransformAction::Comment => {
4325                    // Comment out the code
4326                    format!("// {}", original_text.replace("\n", "\n// "))
4327                }
4328                TransformAction::Remove => {
4329                    // Remove the entire node
4330                    String::new()
4331                }
4332                TransformAction::Replace { with } => {
4333                    // Replace with provided code
4334                    with.clone()
4335                }
4336            };
4337
4338            // Replace in content
4339            self.content.replace_range(start_offset..end_offset, &replacement);
4340
4341            // Recompute line offsets after each change
4342            self.line_offsets = Self::compute_line_offsets(&self.content);
4343
4344            modified_nodes.push(backup_node);
4345        }
4346
4347        // Re-parse the content if we made changes
4348        if !modified_nodes.is_empty() {
4349            // Don't reparse for now - we're doing text-level operations
4350            // self.syntax_tree = syn::parse_str(&self.content)
4351            //     .context("Failed to re-parse content after transformation")?;
4352        }
4353
4354        Ok(ModificationResult {
4355            changed: !modified_nodes.is_empty(),
4356            modified_nodes,
4357            unmatched_qualified_paths: None,
4358        })
4359    }
4360
4361    /// Add an argument to function or method calls
4362    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        // Sort by position (backwards) to avoid offset issues
4374        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            // Create backup node
4384            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            // Parse the call to get argument info
4392            let (args_start, args_end, arg_count) = self.find_call_args_span(&call_match)?;
4393
4394            // Calculate the insert position
4395            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            // Build the new argument text
4402            let new_arg = if arg_count == 0 {
4403                // No existing args - just insert the argument
4404                op.arg_expr.clone()
4405            } else if insert_idx == 0 {
4406                // Inserting at the start
4407                format!("{}, ", op.arg_expr)
4408            } else if insert_idx >= arg_count {
4409                // Inserting at the end
4410                format!(", {}", op.arg_expr)
4411            } else {
4412                // Inserting in the middle
4413                format!("{}, ", op.arg_expr)
4414            };
4415
4416            // Find the insertion point
4417            let insert_offset = if arg_count == 0 {
4418                // Empty args - insert after opening paren
4419                args_start
4420            } else if insert_idx == 0 {
4421                // Insert at start
4422                args_start
4423            } else if insert_idx >= arg_count {
4424                // Insert at end
4425                args_end
4426            } else {
4427                // Find the position after the nth argument's comma
4428                self.find_arg_boundary_offset(&call_match, insert_idx)?
4429            };
4430
4431            // Insert the new argument
4432            self.content.insert_str(insert_offset, &new_arg);
4433
4434            // Recompute line offsets after each change
4435            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    /// Update an argument at a specific index in function or method calls
4448    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        // Sort by position (backwards) to avoid offset issues
4460        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            // Get argument info
4470            let (_, _, arg_count) = self.find_call_args_span(&call_match)?;
4471
4472            // Check if the index is valid
4473            if op.arg_index >= arg_count {
4474                continue; // Skip this call - doesn't have enough arguments
4475            }
4476
4477            // Create backup node
4478            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            // Find the span of the argument to replace
4486            let (arg_start, arg_end) = self.find_arg_span(&call_match, op.arg_index)?;
4487
4488            // Replace the argument
4489            self.content.replace_range(arg_start..arg_end, &op.new_expr);
4490
4491            // Recompute line offsets after each change
4492            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    /// Remove an argument at a specific index from function or method calls
4505    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        // Sort by position (backwards) to avoid offset issues
4517        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            // Get argument info
4527            let (_, _, arg_count) = self.find_call_args_span(&call_match)?;
4528
4529            // Check if the index is valid
4530            if op.arg_index >= arg_count {
4531                continue; // Skip this call - doesn't have enough arguments
4532            }
4533
4534            // Create backup node
4535            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            // Find the span of the argument to remove (including comma)
4543            let (remove_start, remove_end) = self.find_arg_span_with_comma(&call_match, op.arg_index, arg_count)?;
4544
4545            // Remove the argument
4546            self.content.replace_range(remove_start..remove_end, "");
4547
4548            // Recompute line offsets after each change
4549            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    /// Find all function and/or method call sites matching the given name
4562    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        // Determine which types to search
4566        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        // Apply content filter if specified
4580        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    /// Find the byte offsets of the arguments list (just after opening paren, just before closing paren)
4590    /// Returns (start_offset, end_offset, arg_count)
4591    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        // Find the opening parenthesis
4604        let paren_start = call_text.find('(')
4605            .ok_or_else(|| anyhow::anyhow!("Could not find opening parenthesis in call"))?;
4606
4607        // Find the closing parenthesis (accounting for nested parens)
4608        let paren_end = self.find_matching_paren(call_text, paren_start)?;
4609
4610        // Extract just the arguments text
4611        let args_text = &call_text[paren_start + 1..paren_end];
4612
4613        // Count arguments (handle nested parens, brackets, braces)
4614        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    /// Find matching closing parenthesis, accounting for nesting
4624    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    /// Count the number of arguments in an argument list string
4674    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    /// Find the byte offset of the boundary before the nth argument
4724    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                        // Return position right after the comma (and any whitespace)
4767                        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        // If we didn't find the boundary, return the end
4777        Ok(args_end)
4778    }
4779
4780    /// Find the byte span of a specific argument
4781    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        // Skip leading whitespace for first arg
4793        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                        // Found the end of our argument
4830                        // Trim trailing whitespace from the argument
4831                        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                    // Next arg starts after comma and whitespace
4837                    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        // Last argument - goes to end of args
4846        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    /// Find the byte span of a specific argument including comma for removal
4856    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        // Skip leading whitespace for first arg
4868        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                        // Found our argument
4905                        if arg_index == 0 && arg_count > 1 {
4906                            // First arg, but not only - include comma and whitespace after
4907                            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                            // Include comma (and preceding whitespace from previous arg separator)
4912                            return Ok((args_start + arg_start_in_text, args_start + i + 1));
4913                        }
4914                    }
4915                    current_arg += 1;
4916                    // Next arg starts after comma and whitespace
4917                    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        // Last argument
4926        if current_arg == arg_index {
4927            if arg_count > 1 {
4928                // Not the only arg - need to remove preceding comma too
4929                // Find the comma before this arg
4930                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                // Only argument - just remove it
4936                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    /// Rename an enum variant across the entire file
4945    pub(crate) fn rename_enum_variant(&mut self, op: &crate::operations::RenameEnumVariantOp) -> Result<ModificationResult> {
4946        use crate::operations::EditMode;
4947
4948        // Create a path resolver if a canonical path was provided
4949        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            // Scan the file for use statements to build the alias map
4954            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 non-mutating visitor to collect replacement locations
4963                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                // Apply surgical edits to original content
4985                self.content = crate::surgical::apply_surgical_edits(&self.content, collector.replacements);
4986
4987                // Recompute line offsets
4988                self.line_offsets = Self::compute_line_offsets(&self.content);
4989
4990                // Re-parse the modified content
4991                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                // Use mutating visitor (original behavior)
5014                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                // Visit and mutate the syntax tree
5023                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                // Reformat the entire file using prettyplease
5034                self.content = prettyplease::unparse(&self.syntax_tree);
5035
5036                // Recompute line offsets
5037                self.line_offsets = Self::compute_line_offsets(&self.content);
5038
5039                // Create a backup node for the entire file operation
5040                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    /// Rename a function across the entire file
5062    pub(crate) fn rename_function(&mut self, op: &crate::operations::RenameFunctionOp) -> Result<ModificationResult> {
5063        use crate::operations::EditMode;
5064
5065        // Create a path resolver if a canonical path was provided
5066        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            // Scan the file for use statements to build the alias map
5071            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 non-mutating visitor to collect replacement locations
5080                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                // Apply surgical edits to original content
5100                self.content = crate::surgical::apply_surgical_edits(&self.content, collector.replacements);
5101
5102                // Recompute line offsets
5103                self.line_offsets = Self::compute_line_offsets(&self.content);
5104
5105                // Re-parse the modified content
5106                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                // Use mutating visitor (original behavior)
5129                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                // Visit and mutate the syntax tree
5137                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                // Reformat the entire file using prettyplease
5148                self.content = prettyplease::unparse(&self.syntax_tree);
5149
5150                // Recompute line offsets
5151                self.line_offsets = Self::compute_line_offsets(&self.content);
5152
5153                // Create a backup node for the entire file operation
5154                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    /// Convert line/column to byte offset
5176    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
5186// Visitor for adding match arms
5187struct 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        // Continue visiting nested items
5201        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        // Check if we're in the right function (if specified)
5208        if let Some(ref target) = self.target_function {
5209            if self.current_function.as_ref() != Some(target) {
5210                // Continue visiting nested expressions
5211                syn::visit_mut::visit_expr_match_mut(self, node);
5212                return;
5213            }
5214        }
5215
5216        // Check if the pattern already exists (idempotent)
5217        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            // Add the arm to the end
5224            node.arms.push(self.arm_to_add.clone());
5225            self.modified = true;
5226            self.modified_function = self.current_function.clone();
5227        }
5228
5229        // Continue visiting nested expressions
5230        syn::visit_mut::visit_expr_match_mut(self, node);
5231    }
5232}
5233
5234// Visitor for updating match arms
5235struct 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        // Check if we're in the right function (if specified)
5256        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        // Find and update the matching arm
5264        for arm in &mut node.arms {
5265            let pattern_str = arm.pat.to_token_stream().to_string();
5266            // Normalize whitespace for comparison
5267            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
5282// Visitor for removing match arms
5283struct 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        // Check if we're in the right function (if specified)
5303        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        // Find and remove the matching arm
5311        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            // Normalize whitespace for comparison
5315            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
5334// Visitor for adding multiple match arms at once (for auto-detect)
5335struct MultiMatchArmAdder {
5336    target_function: Option<String>,
5337    arms_to_add: Vec<(String, Arm)>,  // (pattern_string, arm)
5338    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        // Check if we're in the right function (if specified)
5355        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        // Add all missing arms
5363        for (pattern_str, arm) in &self.arms_to_add {
5364            // Check if the pattern already exists (idempotent)
5365            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
5380// Visitor for adding fields to struct literal expressions
5381struct 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        // Check if this is a struct literal expression
5393        if let Expr::Struct(expr_struct) = node {
5394            let is_match = if let Some(resolver) = &self.path_resolver {
5395                // Use PathResolver for safe matching
5396                resolver.matches_target(&expr_struct.path)
5397            } else {
5398                // Fallback to legacy pattern matching
5399                // - "Rectangle" → only Rectangle { ... } (no :: prefix)
5400                // - "*::Rectangle" → any path ending with Rectangle (View::Rectangle, etc.)
5401                // - "View::Rectangle" → exact match only View::Rectangle
5402
5403                if self.struct_name.contains("::") {
5404                    // Pattern contains :: - check for exact or wildcard match
5405                    if self.struct_name.starts_with("*::") {
5406                        // Wildcard: *::Rectangle matches any path ending with Rectangle
5407                        let target_name = &self.struct_name[3..]; // Skip "*::"
5408                        expr_struct.path.segments.last()
5409                            .map(|seg| seg.ident.to_string() == target_name)
5410                            .unwrap_or(false)
5411                    } else {
5412                        // Exact path match: View::Rectangle
5413                        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                    // No :: in pattern - only match pure struct literals (no path qualifier)
5421                    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                // Check if field already exists (idempotent)
5430                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                    // Parse the field value from field_def
5436                    // field_def is like "return_type: None"
5437                    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                            // Determine where to insert
5441                            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                                    // Find the position of the field to insert after
5452                                    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                                    // Find the position of the field to insert before
5461                                    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        // IMPORTANT: Visit children AFTER processing this node
5476        // This ensures we traverse into nested expressions
5477        syn::visit_mut::visit_expr_mut(self, node);
5478    }
5479}
5480
5481// Visitor for renaming enum variants
5482struct 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    /// Rename a path segment if it matches using path resolution.
5492    ///
5493    /// This method handles various path forms:
5494    /// - Simple paths: `EnumName::Variant`
5495    /// - Qualified paths: `crate::module::EnumName::Variant`
5496    /// - Imported paths: `Variant` (when enum is imported via use statement)
5497    ///
5498    /// When a PathResolver is configured, it validates that paths refer to
5499    /// the correct enum before renaming.
5500    fn rename_path(&mut self, path: &mut syn::Path) {
5501        // Check if the path ends with EnumName::VariantName
5502        let segments: Vec<_> = path.segments.iter().collect();
5503        let len = segments.len();
5504
5505        if len >= 2 {
5506            // Path has at least enum and variant segments
5507            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                // Path ends with EnumName::VariantName
5514
5515                // If we have a path resolver, validate the enum path
5516                if let Some(resolver) = &self.path_resolver {
5517                    // Extract just the enum path (everything except the variant)
5518                    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                    // Only rename if the enum path matches our target
5527                    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                    // No resolver - use simple matching (backward compatible)
5536                    // Only match if it's exactly EnumName::Variant (2 segments)
5537                    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            // Single segment path - check if it's an imported variant
5548            if segments[0].ident == self.old_variant {
5549                // When using path resolver, we don't rename single-segment paths
5550                // unless they're explicitly imported (which would be unusual for variants)
5551                // For backward compatibility, we still rename them when no resolver is present
5552                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    /// Rename in enum definition
5566    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        // Continue visiting nested items
5577        syn::visit_mut::visit_item_enum_mut(self, node);
5578    }
5579
5580    /// Rename in patterns (match arms, let bindings, function parameters, etc.)
5581    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        // Continue visiting nested patterns
5596        syn::visit_mut::visit_pat_mut(self, pat);
5597    }
5598
5599    /// Rename in expressions (constructor calls, references, etc.)
5600    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        // Continue visiting nested expressions
5617        syn::visit_mut::visit_expr_mut(self, expr);
5618    }
5619}
5620
5621// Non-mutating visitor for collecting replacement locations (surgical mode)
5622struct 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    /// Check if a path matches and collect replacement if it does
5632    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                // Path ends with EnumName::VariantName
5644
5645                // Validate with path resolver if available
5646                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                    // No resolver - only match exactly 2 segments
5657                    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            // Single segment - only without path resolver (backward compat)
5674            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
5744// Mutating visitor for renaming functions (reformat mode)
5745struct FunctionRenamer {
5746    old_name: String,
5747    new_name: String,
5748    path_resolver: Option<PathResolver>,
5749    modified: bool,
5750}
5751
5752impl FunctionRenamer {
5753    /// Rename a function identifier if it matches
5754    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    /// Check if a path matches our target function (with path resolution)
5762    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            // Simple matching: just check if the last segment is our function name
5767            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        // Rename function definition
5775        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        // Rename impl method definition
5781        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        // Rename trait method definition
5787        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                // Rename function calls
5795                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                // Rename function references (not calls)
5805                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
5817// Non-mutating visitor for collecting function replacement locations (surgical mode)
5818struct 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    /// Collect replacement for a function identifier
5827    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    /// Check if a path matches our target function
5842    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            // Simple matching
5847            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        // Collect function definition rename
5855        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        // Collect impl method definition rename
5861        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        // Collect trait method definition rename
5867        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                // Collect function call renames
5875                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                // Visit call arguments but NOT the func (already handled above)
5883                for arg in &call.args {
5884                    syn::visit::visit_expr(self, arg);
5885                }
5886                // Don't call the default visitor which would re-visit call.func
5887                return;
5888            }
5889            syn::Expr::Path(expr_path) => {
5890                // Collect function reference renames (not calls)
5891                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
5903// ============================================================================
5904// Doc Comment Operations
5905// ============================================================================
5906
5907/// Generate a documentation comment in the specified style
5908fn generate_doc_comment(text: &str, style: &DocCommentStyle) -> String {
5909    match style {
5910        DocCommentStyle::Line => {
5911            // Split by newlines and add /// prefix to each line
5912            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            // Simple block comment
5925            if text.contains('\n') {
5926                // Multi-line block comment
5927                let lines = text.lines()
5928                    .map(|line| format!(" * {}", line))
5929                    .collect::<Vec<_>>()
5930                    .join("\n");
5931                format!("/**\n{}\n */", lines)
5932            } else {
5933                // Single-line block comment
5934                format!("/** {} */", text)
5935            }
5936        }
5937    }
5938}
5939
5940/// Extract preceding comments (both doc and regular) before a given line
5941/// Returns None if no comments found, Some(comment_text) if comments exist
5942fn 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); // Convert 1-based to 0-based
5953
5954    // Scan backwards from target to find comment lines
5955    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        // Check if this is a comment line
5962        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            // Allow blank lines within comment blocks, but don't continue past them
5976            // unless we're in the middle of a block comment
5977            comment_start -= 1;
5978        } else {
5979            break;
5980        }
5981    }
5982
5983    if !found_any_comment {
5984        return None;
5985    }
5986
5987    // Extract the comment lines and preserve original formatting
5988    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
6000/// Find the byte position and indentation of a target item
6001struct TargetFinder {
6002    target_type: String,
6003    target_name: String,
6004    found_position: Option<(usize, String)>, // (line_number, indentation)
6005}
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            // Use struct_token span to get the line where "struct" keyword appears,
6021            // not the first line of the item (which includes doc comments)
6022            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            // Use enum_token span to get the line where "enum" keyword appears
6031            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            // Use fn_token span to get the line where "fn" keyword appears
6040            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    /// Add a documentation comment to a target item using surgical editing
6049    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        // Find the target item
6059        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            // Lines are 1-indexed from syn, convert to 0-indexed
6067            let line_idx = line_num.saturating_sub(1);
6068
6069            // Generate the doc comment
6070            let comment = generate_doc_comment(doc_text, style);
6071
6072            // Find the actual line in the source
6073            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(); // Clone to avoid borrow issues
6079            let target_line_len = target_line.len();
6080
6081            // Detect indentation from the target line
6082            let indent = target_line
6083                .chars()
6084                .take_while(|c| c.is_whitespace())
6085                .collect::<String>();
6086
6087            // Build the new content with comment inserted
6088            let mut new_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
6089
6090            // Insert comment lines before the target
6091            let comment_lines: Vec<String> = comment
6092                .lines()
6093                .map(|line| format!("{}{}", indent, line))
6094                .collect();
6095
6096            // Insert in reverse order to maintain indices
6097            for (i, comment_line) in comment_lines.iter().rev().enumerate() {
6098                new_lines.insert(line_idx, comment_line.clone());
6099            }
6100
6101            // Update content
6102            self.content = new_lines.join("\n");
6103
6104            // Re-parse to update syntax tree
6105            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    /// Update an existing documentation comment on a target item using surgical editing
6129    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        // Find the target item
6139        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            // Lines are 1-indexed from syn, convert to 0-indexed
6147            let line_idx = line_num.saturating_sub(1);
6148
6149            // Find the actual line in the source
6150            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(); // Clone to avoid borrow issues
6156            let target_line_len = target_line.len();
6157
6158            // Detect indentation from the target line
6159            let indent = target_line
6160                .chars()
6161                .take_while(|c| c.is_whitespace())
6162                .collect::<String>();
6163
6164            // Scan backwards from target to find existing doc comment lines
6165            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            // Build new content with old comments removed and new ones inserted
6179            let mut new_lines: Vec<String> = Vec::new();
6180
6181            // Add lines before the doc comments
6182            for i in 0..doc_comment_start {
6183                new_lines.push(lines[i].to_string());
6184            }
6185
6186            // Generate and add new doc comment
6187            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            // Add remaining lines (from target onwards)
6198            for i in line_idx..lines.len() {
6199                new_lines.push(lines[i].to_string());
6200            }
6201
6202            // Update content
6203            self.content = new_lines.join("\n");
6204
6205            // Re-parse to update syntax tree
6206            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    /// Remove a documentation comment from a target item using surgical editing
6230    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        // Find the target item
6238        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            // Lines are 1-indexed from syn, convert to 0-indexed
6246            let line_idx = line_num.saturating_sub(1);
6247
6248            // Find the actual line in the source
6249            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(); // Clone to avoid borrow issues
6255            let target_line_len = target_line.len();
6256
6257            // Scan backwards from target to find existing doc comment lines
6258            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            // Build new content with doc comments removed
6272            let mut new_lines: Vec<String> = Vec::new();
6273
6274            // Add lines before the doc comments
6275            for i in 0..doc_comment_start {
6276                new_lines.push(lines[i].to_string());
6277            }
6278
6279            // Skip the doc comment lines (from doc_comment_start to line_idx)
6280
6281            // Add remaining lines (from target onwards)
6282            for i in line_idx..lines.len() {
6283                new_lines.push(lines[i].to_string());
6284            }
6285
6286            // Update content
6287            self.content = new_lines.join("\n");
6288
6289            // Re-parse to update syntax tree
6290            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}