normalize_refactor/
rename.rs1use std::collections::HashMap;
10
11use crate::actions;
12use crate::{PlannedEdit, RefactoringContext, RefactoringPlan};
13
14pub async fn plan_rename(
19 ctx: &RefactoringContext,
20 def_rel_path: &str,
21 old_name: &str,
22 new_name: &str,
23 force: bool,
24) -> Result<RefactoringPlan, String> {
25 let def_rel_path = def_rel_path.to_string();
26 let def_abs_path = ctx.root.join(&def_rel_path);
27
28 let def_content = std::fs::read_to_string(&def_abs_path)
29 .map_err(|e| format!("Error reading {}: {}", def_rel_path, e))?;
30
31 let loc = actions::locate_symbol(ctx, &def_abs_path, &def_content, old_name)
33 .ok_or_else(|| format!("Symbol '{}' not found in {}", old_name, def_rel_path))?;
34
35 let refs = actions::find_references(ctx, old_name, &def_rel_path).await;
37
38 if !force {
40 let conflicts =
41 actions::check_conflicts(ctx, &def_abs_path, &def_content, new_name, &refs.importers)
42 .await;
43 if !conflicts.is_empty() {
44 let detail = conflicts
45 .iter()
46 .map(|c| format!(" {}", c))
47 .collect::<Vec<_>>()
48 .join("\n");
49 return Err(format!(
50 "Rename '{}' → '{}' would cause conflicts (use --force to override):\n{}",
51 old_name, new_name, detail
52 ));
53 }
54 }
55
56 let mut edits: Vec<PlannedEdit> = vec![];
57 let mut warnings: Vec<String> = vec![];
58
59 if let Some(edit) = actions::plan_rename_in_file(
61 ctx,
62 &def_abs_path,
63 &def_content,
64 &[loc.start_line],
65 old_name,
66 new_name,
67 ) {
68 edits.push(edit);
69 }
70
71 let mut callers_by_file: HashMap<String, Vec<usize>> = HashMap::new();
73 for caller in &refs.callers {
74 callers_by_file
75 .entry(caller.file.clone())
76 .or_default()
77 .push(caller.line);
78 }
79
80 let mut edited_files: std::collections::HashSet<String> = std::collections::HashSet::new();
82 edited_files.insert(def_rel_path.clone());
83
84 for (rel_path, lines) in &callers_by_file {
85 if rel_path == &def_rel_path {
86 continue;
88 }
89 let abs_path = ctx.root.join(rel_path);
90 let content = match std::fs::read_to_string(&abs_path) {
91 Ok(c) => c,
92 Err(_) => {
93 warnings.push(format!("Could not read caller file: {}", rel_path));
94 continue;
95 }
96 };
97 if let Some(edit) =
98 actions::plan_rename_in_file(ctx, &abs_path, &content, lines, old_name, new_name)
99 {
100 edits.push(edit);
101 edited_files.insert(rel_path.clone());
102 }
103 }
104
105 let mut importers_by_file: HashMap<String, Vec<usize>> = HashMap::new();
107 for imp in &refs.importers {
108 importers_by_file
109 .entry(imp.file.clone())
110 .or_default()
111 .push(imp.line);
112 }
113
114 for (rel_path, lines) in &importers_by_file {
115 if edited_files.contains(rel_path) {
116 if let Some(existing) = edits.iter_mut().find(|e| e.file == ctx.root.join(rel_path)) {
119 let mut current = existing.new_content.clone();
121 let mut changed = false;
122 for &line_no in lines {
123 if let Some(new_content) = ctx
124 .editor
125 .rename_identifier_in_line(¤t, line_no, old_name, new_name)
126 {
127 current = new_content;
128 changed = true;
129 }
130 }
131 if changed {
132 existing.new_content = current;
133 }
134 continue;
135 }
136 }
137 let abs_path = ctx.root.join(rel_path);
138 let content = match std::fs::read_to_string(&abs_path) {
139 Ok(c) => c,
140 Err(_) => {
141 warnings.push(format!("Could not read importer file: {}", rel_path));
142 continue;
143 }
144 };
145 if let Some(edit) =
146 actions::plan_rename_in_file(ctx, &abs_path, &content, lines, old_name, new_name)
147 {
148 edits.push(edit);
149 }
150 }
151
152 if ctx.index.is_none() && refs.callers.is_empty() && refs.importers.is_empty() {
154 warnings.push("Index not available; renamed definition only".to_string());
155 }
156
157 Ok(RefactoringPlan {
158 operation: "rename".to_string(),
159 edits,
160 warnings,
161 })
162}