php_lsp/editing/
rename.rs1use std::collections::HashMap;
2use std::sync::Arc;
3
4use tower_lsp::lsp_types::{Position, Range, TextEdit, Url, WorkspaceEdit};
5
6use crate::document::ast::ParsedDoc;
7use crate::navigation::references::find_references_with_use;
8use crate::navigation::walk::{collect_var_refs_in_scope, property_refs_in_stmts};
9use crate::text::utf16_code_units;
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::navigation::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::text::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 if is_superglobal(&word) {
55 return None;
56 }
57 let line = source.lines().nth(position.line as usize)?;
58 let col = position.character as usize;
59 let chars: Vec<char> = line.chars().collect();
60 let is_word = |c: char| c.is_alphanumeric() || c == '_';
64
65 let mut utf16_col = 0usize;
67 let mut char_idx = 0usize;
68 for (i, ch) in chars.iter().enumerate() {
69 let char_width = ch.len_utf16();
71 if utf16_col + char_width > col {
72 char_idx = i;
73 break;
74 }
75 utf16_col += char_width;
76 char_idx = i + 1;
77 }
78
79 let mut left = char_idx;
81 while left > 0 && is_word(chars[left - 1]) {
82 left -= 1;
83 }
84
85 let bare_word = word.trim_start_matches('$');
86 let start_utf16: u32 = chars[..left].iter().map(|c| c.len_utf16() as u32).sum();
87 let end_utf16: u32 = start_utf16 + utf16_code_units(bare_word);
88 Some(Range {
89 start: Position {
90 line: position.line,
91 character: start_utf16,
92 },
93 end: Position {
94 line: position.line,
95 character: end_utf16,
96 },
97 })
98}
99
100fn is_php_keyword(word: &str) -> bool {
101 matches!(
102 word,
103 "abstract"
104 | "and"
105 | "array"
106 | "as"
107 | "break"
108 | "callable"
109 | "case"
110 | "catch"
111 | "class"
112 | "clone"
113 | "const"
114 | "continue"
115 | "declare"
116 | "default"
117 | "die"
118 | "do"
119 | "echo"
120 | "else"
121 | "elseif"
122 | "empty"
123 | "enddeclare"
124 | "endfor"
125 | "endforeach"
126 | "endif"
127 | "endswitch"
128 | "endwhile"
129 | "enum"
130 | "eval"
131 | "exit"
132 | "extends"
133 | "final"
134 | "finally"
135 | "fn"
136 | "for"
137 | "foreach"
138 | "function"
139 | "global"
140 | "goto"
141 | "if"
142 | "implements"
143 | "include"
144 | "include_once"
145 | "instanceof"
146 | "insteadof"
147 | "interface"
148 | "isset"
149 | "list"
150 | "match"
151 | "namespace"
152 | "new"
153 | "null"
154 | "or"
155 | "print"
156 | "private"
157 | "protected"
158 | "public"
159 | "readonly"
160 | "require"
161 | "require_once"
162 | "return"
163 | "self"
164 | "static"
165 | "switch"
166 | "throw"
167 | "trait"
168 | "true"
169 | "false"
170 | "try"
171 | "use"
172 | "var"
173 | "while"
174 | "xor"
175 | "yield"
176 )
177}
178
179fn is_superglobal(word: &str) -> bool {
180 matches!(
181 word,
182 "$_GET"
183 | "$_POST"
184 | "$_REQUEST"
185 | "$_FILES"
186 | "$_COOKIE"
187 | "$_SESSION"
188 | "$_SERVER"
189 | "$_ENV"
190 | "$GLOBALS"
191 )
192}
193
194pub fn rename_variable(
197 var_name: &str,
198 new_name: &str,
199 uri: &Url,
200 doc: &ParsedDoc,
201 position: Position,
202) -> WorkspaceEdit {
203 let bare = var_name.trim_start_matches('$');
204 let new_bare = new_name.trim_start_matches('$');
205 let new_text = format!("${new_bare}");
206
207 let stmts = &doc.program().stmts;
208 let sv = doc.view();
209 let byte_off = sv.byte_of_position(position) as usize;
210
211 let mut spans = Vec::new();
212 collect_var_refs_in_scope(stmts, bare, byte_off, &mut spans);
213
214 let mut seen = std::collections::HashSet::new();
215 let mut edits: Vec<TextEdit> = spans
216 .into_iter()
217 .filter_map(|(span, _)| {
218 let start = sv.position_of(span.start);
219 let end = sv.position_of(span.end);
220 seen.insert((start.line, start.character))
221 .then_some(TextEdit {
222 range: Range { start, end },
223 new_text: new_text.clone(),
224 })
225 })
226 .collect();
227 edits.sort_by_key(|e| (e.range.start.line, e.range.start.character));
228
229 let mut changes = HashMap::new();
230 if !edits.is_empty() {
231 changes.insert(uri.clone(), edits);
232 }
233
234 WorkspaceEdit {
235 changes: if changes.is_empty() {
236 None
237 } else {
238 Some(changes)
239 },
240 ..Default::default()
241 }
242}
243
244pub fn rename_property(
248 prop_name: &str,
249 new_name: &str,
250 all_docs: &[(Url, Arc<ParsedDoc>)],
251) -> WorkspaceEdit {
252 let mut changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();
253 for (uri, doc) in all_docs {
254 let sv = doc.view();
255 let mut spans = Vec::new();
256 property_refs_in_stmts(sv.source(), &doc.program().stmts, prop_name, &mut spans);
257 if !spans.is_empty() {
258 let mut seen = std::collections::HashSet::new();
259 let mut edits: Vec<TextEdit> = spans
260 .into_iter()
261 .filter_map(|span| {
262 let start = sv.position_of(span.start);
263 let end = sv.position_of(span.end);
264 seen.insert((start.line, start.character))
265 .then_some(TextEdit {
266 range: Range { start, end },
267 new_text: new_name.to_string(),
268 })
269 })
270 .collect();
271 edits.sort_by_key(|e| (e.range.start.line, e.range.start.character));
272 changes.insert(uri.clone(), edits);
273 }
274 }
275 WorkspaceEdit {
276 changes: if changes.is_empty() {
277 None
278 } else {
279 Some(changes)
280 },
281 ..Default::default()
282 }
283}