1use std::collections::HashMap;
2use std::sync::Arc;
3
4use tower_lsp::lsp_types::{Position, Range, TextEdit, Url, WorkspaceEdit};
5
6use crate::ast::ParsedDoc;
7use crate::references::find_references_with_use;
8use crate::util::utf16_code_units;
9use crate::walk::{collect_var_refs_in_scope, property_refs_in_stmts};
10
11pub fn rename(
14 word: &str,
15 new_name: &str,
16 all_docs: &[(Url, Arc<ParsedDoc>)],
17 target_fqn: Option<&str>,
18) -> WorkspaceEdit {
19 use crate::references::find_references_with_target;
20
21 let locations = match target_fqn {
22 Some(fqn) => find_references_with_target(word, all_docs, true, None, fqn),
23 None => find_references_with_use(word, all_docs, true),
24 };
25
26 let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
27 for loc in locations {
28 changes.entry(loc.uri).or_default().push(TextEdit {
29 range: loc.range,
30 new_text: new_name.to_string(),
31 });
32 }
33
34 WorkspaceEdit {
35 changes: Some(changes),
36 ..Default::default()
37 }
38}
39
40pub fn prepare_rename(source: &str, position: Position) -> Option<Range> {
43 use crate::util::word_at_position;
44 let word = word_at_position(source, position)?;
45 if word.contains('\\') {
46 return None;
47 }
48 if is_php_keyword(&word) {
50 return None;
51 }
52 let line = source.lines().nth(position.line as usize)?;
53 let col = position.character as usize;
54 let chars: Vec<char> = line.chars().collect();
55 let is_word = |c: char| c.is_alphanumeric() || c == '_';
59
60 let mut utf16_col = 0usize;
62 let mut char_idx = 0usize;
63 for (i, ch) in chars.iter().enumerate() {
64 let char_width = ch.len_utf16();
66 if utf16_col + char_width > col {
67 char_idx = i;
68 break;
69 }
70 utf16_col += char_width;
71 char_idx = i + 1;
72 }
73
74 let mut left = char_idx;
76 while left > 0 && is_word(chars[left - 1]) {
77 left -= 1;
78 }
79
80 let bare_word = word.trim_start_matches('$');
81 let start_utf16: u32 = chars[..left].iter().map(|c| c.len_utf16() as u32).sum();
82 let end_utf16: u32 = start_utf16 + utf16_code_units(bare_word);
83 Some(Range {
84 start: Position {
85 line: position.line,
86 character: start_utf16,
87 },
88 end: Position {
89 line: position.line,
90 character: end_utf16,
91 },
92 })
93}
94
95fn is_php_keyword(word: &str) -> bool {
96 matches!(
97 word,
98 "abstract"
99 | "and"
100 | "array"
101 | "as"
102 | "break"
103 | "callable"
104 | "case"
105 | "catch"
106 | "class"
107 | "clone"
108 | "const"
109 | "continue"
110 | "declare"
111 | "default"
112 | "die"
113 | "do"
114 | "echo"
115 | "else"
116 | "elseif"
117 | "empty"
118 | "enddeclare"
119 | "endfor"
120 | "endforeach"
121 | "endif"
122 | "endswitch"
123 | "endwhile"
124 | "enum"
125 | "eval"
126 | "exit"
127 | "extends"
128 | "final"
129 | "finally"
130 | "fn"
131 | "for"
132 | "foreach"
133 | "function"
134 | "global"
135 | "goto"
136 | "if"
137 | "implements"
138 | "include"
139 | "include_once"
140 | "instanceof"
141 | "insteadof"
142 | "interface"
143 | "isset"
144 | "list"
145 | "match"
146 | "namespace"
147 | "new"
148 | "null"
149 | "or"
150 | "print"
151 | "private"
152 | "protected"
153 | "public"
154 | "readonly"
155 | "require"
156 | "require_once"
157 | "return"
158 | "self"
159 | "static"
160 | "switch"
161 | "throw"
162 | "trait"
163 | "true"
164 | "false"
165 | "try"
166 | "use"
167 | "var"
168 | "while"
169 | "xor"
170 | "yield"
171 )
172}
173
174pub fn rename_variable(
177 var_name: &str,
178 new_name: &str,
179 uri: &Url,
180 doc: &ParsedDoc,
181 position: Position,
182) -> WorkspaceEdit {
183 let bare = var_name.trim_start_matches('$');
184 let new_bare = new_name.trim_start_matches('$');
185 let new_text = format!("${new_bare}");
186
187 let stmts = &doc.program().stmts;
188 let sv = doc.view();
189 let byte_off = sv.byte_of_position(position) as usize;
190
191 let mut spans = Vec::new();
192 collect_var_refs_in_scope(stmts, bare, byte_off, &mut spans);
193
194 let mut seen = std::collections::HashSet::new();
195 let mut edits: Vec<TextEdit> = spans
196 .into_iter()
197 .filter_map(|(span, _)| {
198 let start = sv.position_of(span.start);
199 let end = sv.position_of(span.end);
200 seen.insert((start.line, start.character))
201 .then_some(TextEdit {
202 range: Range { start, end },
203 new_text: new_text.clone(),
204 })
205 })
206 .collect();
207 edits.sort_by_key(|e| (e.range.start.line, e.range.start.character));
208
209 let mut changes = HashMap::new();
210 if !edits.is_empty() {
211 changes.insert(uri.clone(), edits);
212 }
213
214 WorkspaceEdit {
215 changes: if changes.is_empty() {
216 None
217 } else {
218 Some(changes)
219 },
220 ..Default::default()
221 }
222}
223
224pub fn rename_property(
228 prop_name: &str,
229 new_name: &str,
230 all_docs: &[(Url, Arc<ParsedDoc>)],
231) -> WorkspaceEdit {
232 let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
233 for (uri, doc) in all_docs {
234 let sv = doc.view();
235 let mut spans = Vec::new();
236 property_refs_in_stmts(sv.source(), &doc.program().stmts, prop_name, &mut spans);
237 if !spans.is_empty() {
238 let mut seen = std::collections::HashSet::new();
239 let mut edits: Vec<TextEdit> = spans
240 .into_iter()
241 .filter_map(|span| {
242 let start = sv.position_of(span.start);
243 let end = sv.position_of(span.end);
244 seen.insert((start.line, start.character))
245 .then_some(TextEdit {
246 range: Range { start, end },
247 new_text: new_name.to_string(),
248 })
249 })
250 .collect();
251 edits.sort_by_key(|e| (e.range.start.line, e.range.start.character));
252 changes.insert(uri.clone(), edits);
253 }
254 }
255 WorkspaceEdit {
256 changes: if changes.is_empty() {
257 None
258 } else {
259 Some(changes)
260 },
261 ..Default::default()
262 }
263}