diff options
Diffstat (limited to 'crates/ide/src/references/rename.rs')
-rw-r--r-- | crates/ide/src/references/rename.rs | 182 |
1 files changed, 136 insertions, 46 deletions
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs index 8cbe1ae5a..f9a11e43d 100644 --- a/crates/ide/src/references/rename.rs +++ b/crates/ide/src/references/rename.rs | |||
@@ -6,11 +6,16 @@ use ide_db::{ | |||
6 | defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, | 6 | defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, |
7 | RootDatabase, | 7 | RootDatabase, |
8 | }; | 8 | }; |
9 | use std::convert::TryInto; | 9 | |
10 | use std::{ | ||
11 | convert::TryInto, | ||
12 | error::Error, | ||
13 | fmt::{self, Display}, | ||
14 | }; | ||
10 | use syntax::{ | 15 | use syntax::{ |
11 | algo::find_node_at_offset, | 16 | algo::find_node_at_offset, |
12 | ast::{self, NameOwner}, | 17 | ast::{self, NameOwner}, |
13 | lex_single_valid_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken, | 18 | lex_single_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken, |
14 | }; | 19 | }; |
15 | use test_utils::mark; | 20 | use test_utils::mark; |
16 | use text_edit::TextEdit; | 21 | use text_edit::TextEdit; |
@@ -20,17 +25,44 @@ use crate::{ | |||
20 | SourceChange, SourceFileEdit, TextRange, TextSize, | 25 | SourceChange, SourceFileEdit, TextRange, TextSize, |
21 | }; | 26 | }; |
22 | 27 | ||
28 | #[derive(Debug)] | ||
29 | pub struct RenameError(pub(crate) String); | ||
30 | |||
31 | impl fmt::Display for RenameError { | ||
32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
33 | Display::fmt(&self.0, f) | ||
34 | } | ||
35 | } | ||
36 | |||
37 | impl Error for RenameError {} | ||
38 | |||
23 | pub(crate) fn rename( | 39 | pub(crate) fn rename( |
24 | db: &RootDatabase, | 40 | db: &RootDatabase, |
25 | position: FilePosition, | 41 | position: FilePosition, |
26 | new_name: &str, | 42 | new_name: &str, |
27 | ) -> Option<RangeInfo<SourceChange>> { | 43 | ) -> Result<RangeInfo<SourceChange>, RenameError> { |
28 | let sema = Semantics::new(db); | 44 | let sema = Semantics::new(db); |
45 | rename_with_semantics(&sema, position, new_name) | ||
46 | } | ||
29 | 47 | ||
30 | match lex_single_valid_syntax_kind(new_name)? { | 48 | pub(crate) fn rename_with_semantics( |
31 | SyntaxKind::IDENT | SyntaxKind::UNDERSCORE => (), | 49 | sema: &Semantics<RootDatabase>, |
32 | SyntaxKind::SELF_KW => return rename_to_self(&sema, position), | 50 | position: FilePosition, |
33 | _ => return None, | 51 | new_name: &str, |
52 | ) -> Result<RangeInfo<SourceChange>, RenameError> { | ||
53 | match lex_single_syntax_kind(new_name) { | ||
54 | Some(res) => match res { | ||
55 | (SyntaxKind::IDENT, _) => (), | ||
56 | (SyntaxKind::UNDERSCORE, _) => (), | ||
57 | (SyntaxKind::SELF_KW, _) => return rename_to_self(&sema, position), | ||
58 | (_, Some(syntax_error)) => { | ||
59 | return Err(RenameError(format!("Invalid name `{}`: {}", new_name, syntax_error))) | ||
60 | } | ||
61 | (_, None) => { | ||
62 | return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))) | ||
63 | } | ||
64 | }, | ||
65 | None => return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))), | ||
34 | } | 66 | } |
35 | 67 | ||
36 | let source_file = sema.parse(position.file_id); | 68 | let source_file = sema.parse(position.file_id); |
@@ -103,7 +135,7 @@ fn rename_mod( | |||
103 | position: FilePosition, | 135 | position: FilePosition, |
104 | module: Module, | 136 | module: Module, |
105 | new_name: &str, | 137 | new_name: &str, |
106 | ) -> Option<RangeInfo<SourceChange>> { | 138 | ) -> Result<RangeInfo<SourceChange>, RenameError> { |
107 | let mut source_file_edits = Vec::new(); | 139 | let mut source_file_edits = Vec::new(); |
108 | let mut file_system_edits = Vec::new(); | 140 | let mut file_system_edits = Vec::new(); |
109 | 141 | ||
@@ -125,7 +157,7 @@ fn rename_mod( | |||
125 | 157 | ||
126 | if let Some(src) = module.declaration_source(sema.db) { | 158 | if let Some(src) = module.declaration_source(sema.db) { |
127 | let file_id = src.file_id.original_file(sema.db); | 159 | let file_id = src.file_id.original_file(sema.db); |
128 | let name = src.value.name()?; | 160 | let name = src.value.name().unwrap(); |
129 | let edit = SourceFileEdit { | 161 | let edit = SourceFileEdit { |
130 | file_id, | 162 | file_id, |
131 | edit: TextEdit::replace(name.syntax().text_range(), new_name.into()), | 163 | edit: TextEdit::replace(name.syntax().text_range(), new_name.into()), |
@@ -133,35 +165,40 @@ fn rename_mod( | |||
133 | source_file_edits.push(edit); | 165 | source_file_edits.push(edit); |
134 | } | 166 | } |
135 | 167 | ||
136 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; | 168 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None) |
169 | .ok_or_else(|| RenameError("No references found at position".to_string()))?; | ||
137 | let ref_edits = refs | 170 | let ref_edits = refs |
138 | .references | 171 | .references |
139 | .into_iter() | 172 | .into_iter() |
140 | .map(|reference| source_edit_from_reference(reference, new_name)); | 173 | .map(|reference| source_edit_from_reference(reference, new_name)); |
141 | source_file_edits.extend(ref_edits); | 174 | source_file_edits.extend(ref_edits); |
142 | 175 | ||
143 | Some(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits))) | 176 | Ok(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits))) |
144 | } | 177 | } |
145 | 178 | ||
146 | fn rename_to_self( | 179 | fn rename_to_self( |
147 | sema: &Semantics<RootDatabase>, | 180 | sema: &Semantics<RootDatabase>, |
148 | position: FilePosition, | 181 | position: FilePosition, |
149 | ) -> Option<RangeInfo<SourceChange>> { | 182 | ) -> Result<RangeInfo<SourceChange>, RenameError> { |
150 | let source_file = sema.parse(position.file_id); | 183 | let source_file = sema.parse(position.file_id); |
151 | let syn = source_file.syntax(); | 184 | let syn = source_file.syntax(); |
152 | 185 | ||
153 | let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; | 186 | let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset) |
154 | let params = fn_def.param_list()?; | 187 | .ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?; |
188 | let params = | ||
189 | fn_def.param_list().ok_or_else(|| RenameError("Method has no parameters".to_string()))?; | ||
155 | if params.self_param().is_some() { | 190 | if params.self_param().is_some() { |
156 | return None; // method already has self param | 191 | return Err(RenameError("Method already has a self parameter".to_string())); |
157 | } | 192 | } |
158 | let first_param = params.params().next()?; | 193 | let first_param = |
194 | params.params().next().ok_or_else(|| RenameError("Method has no parameters".into()))?; | ||
159 | let mutable = match first_param.ty() { | 195 | let mutable = match first_param.ty() { |
160 | Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(), | 196 | Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(), |
161 | _ => return None, // not renaming other types | 197 | _ => return Err(RenameError("Not renaming other types".to_string())), |
162 | }; | 198 | }; |
163 | 199 | ||
164 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; | 200 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None) |
201 | .ok_or_else(|| RenameError("No reference found at position".to_string()))?; | ||
165 | 202 | ||
166 | let param_range = first_param.syntax().text_range(); | 203 | let param_range = first_param.syntax().text_range(); |
167 | let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs | 204 | let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs |
@@ -169,7 +206,7 @@ fn rename_to_self( | |||
169 | .partition(|reference| param_range.intersect(reference.file_range.range).is_some()); | 206 | .partition(|reference| param_range.intersect(reference.file_range.range).is_some()); |
170 | 207 | ||
171 | if param_ref.is_empty() { | 208 | if param_ref.is_empty() { |
172 | return None; | 209 | return Err(RenameError("Parameter to rename not found".to_string())); |
173 | } | 210 | } |
174 | 211 | ||
175 | let mut edits = usages | 212 | let mut edits = usages |
@@ -185,7 +222,7 @@ fn rename_to_self( | |||
185 | ), | 222 | ), |
186 | }); | 223 | }); |
187 | 224 | ||
188 | Some(RangeInfo::new(range, SourceChange::from(edits))) | 225 | Ok(RangeInfo::new(range, SourceChange::from(edits))) |
189 | } | 226 | } |
190 | 227 | ||
191 | fn text_edit_from_self_param( | 228 | fn text_edit_from_self_param( |
@@ -216,12 +253,13 @@ fn rename_self_to_param( | |||
216 | position: FilePosition, | 253 | position: FilePosition, |
217 | self_token: SyntaxToken, | 254 | self_token: SyntaxToken, |
218 | new_name: &str, | 255 | new_name: &str, |
219 | ) -> Option<RangeInfo<SourceChange>> { | 256 | ) -> Result<RangeInfo<SourceChange>, RenameError> { |
220 | let source_file = sema.parse(position.file_id); | 257 | let source_file = sema.parse(position.file_id); |
221 | let syn = source_file.syntax(); | 258 | let syn = source_file.syntax(); |
222 | 259 | ||
223 | let text = sema.db.file_text(position.file_id); | 260 | let text = sema.db.file_text(position.file_id); |
224 | let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; | 261 | let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset) |
262 | .ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?; | ||
225 | let search_range = fn_def.syntax().text_range(); | 263 | let search_range = fn_def.syntax().text_range(); |
226 | 264 | ||
227 | let mut edits: Vec<SourceFileEdit> = vec![]; | 265 | let mut edits: Vec<SourceFileEdit> = vec![]; |
@@ -235,7 +273,8 @@ fn rename_self_to_param( | |||
235 | syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW) | 273 | syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW) |
236 | { | 274 | { |
237 | let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) { | 275 | let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) { |
238 | text_edit_from_self_param(syn, self_param, new_name)? | 276 | text_edit_from_self_param(syn, self_param, new_name) |
277 | .ok_or_else(|| RenameError("No target type found".to_string()))? | ||
239 | } else { | 278 | } else { |
240 | TextEdit::replace(usage.text_range(), String::from(new_name)) | 279 | TextEdit::replace(usage.text_range(), String::from(new_name)) |
241 | }; | 280 | }; |
@@ -246,15 +285,18 @@ fn rename_self_to_param( | |||
246 | let range = ast::SelfParam::cast(self_token.parent()) | 285 | let range = ast::SelfParam::cast(self_token.parent()) |
247 | .map_or(self_token.text_range(), |p| p.syntax().text_range()); | 286 | .map_or(self_token.text_range(), |p| p.syntax().text_range()); |
248 | 287 | ||
249 | Some(RangeInfo::new(range, SourceChange::from(edits))) | 288 | Ok(RangeInfo::new(range, SourceChange::from(edits))) |
250 | } | 289 | } |
251 | 290 | ||
252 | fn rename_reference( | 291 | fn rename_reference( |
253 | sema: &Semantics<RootDatabase>, | 292 | sema: &Semantics<RootDatabase>, |
254 | position: FilePosition, | 293 | position: FilePosition, |
255 | new_name: &str, | 294 | new_name: &str, |
256 | ) -> Option<RangeInfo<SourceChange>> { | 295 | ) -> Result<RangeInfo<SourceChange>, RenameError> { |
257 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; | 296 | let RangeInfo { range, info: refs } = match find_all_refs(sema, position, None) { |
297 | Some(range_info) => range_info, | ||
298 | None => return Err(RenameError("No references found at position".to_string())), | ||
299 | }; | ||
258 | 300 | ||
259 | let edit = refs | 301 | let edit = refs |
260 | .into_iter() | 302 | .into_iter() |
@@ -262,10 +304,10 @@ fn rename_reference( | |||
262 | .collect::<Vec<_>>(); | 304 | .collect::<Vec<_>>(); |
263 | 305 | ||
264 | if edit.is_empty() { | 306 | if edit.is_empty() { |
265 | return None; | 307 | return Err(RenameError("No references found at position".to_string())); |
266 | } | 308 | } |
267 | 309 | ||
268 | Some(RangeInfo::new(range, SourceChange::from(edit))) | 310 | Ok(RangeInfo::new(range, SourceChange::from(edit))) |
269 | } | 311 | } |
270 | 312 | ||
271 | #[cfg(test)] | 313 | #[cfg(test)] |
@@ -280,25 +322,45 @@ mod tests { | |||
280 | fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | 322 | fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { |
281 | let ra_fixture_after = &trim_indent(ra_fixture_after); | 323 | let ra_fixture_after = &trim_indent(ra_fixture_after); |
282 | let (analysis, position) = fixture::position(ra_fixture_before); | 324 | let (analysis, position) = fixture::position(ra_fixture_before); |
283 | let source_change = analysis.rename(position, new_name).unwrap(); | 325 | let rename_result = analysis |
284 | let mut text_edit_builder = TextEdit::builder(); | 326 | .rename(position, new_name) |
285 | let mut file_id: Option<FileId> = None; | 327 | .unwrap_or_else(|err| panic!("Rename to '{}' was cancelled: {}", new_name, err)); |
286 | if let Some(change) = source_change { | 328 | match rename_result { |
287 | for edit in change.info.source_file_edits { | 329 | Ok(source_change) => { |
288 | file_id = Some(edit.file_id); | 330 | let mut text_edit_builder = TextEdit::builder(); |
289 | for indel in edit.edit.into_iter() { | 331 | let mut file_id: Option<FileId> = None; |
290 | text_edit_builder.replace(indel.delete, indel.insert); | 332 | for edit in source_change.info.source_file_edits { |
333 | file_id = Some(edit.file_id); | ||
334 | for indel in edit.edit.into_iter() { | ||
335 | text_edit_builder.replace(indel.delete, indel.insert); | ||
336 | } | ||
291 | } | 337 | } |
338 | let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string(); | ||
339 | text_edit_builder.finish().apply(&mut result); | ||
340 | assert_eq_text!(ra_fixture_after, &*result); | ||
292 | } | 341 | } |
293 | } | 342 | Err(err) => { |
294 | let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string(); | 343 | if ra_fixture_after.starts_with("error:") { |
295 | text_edit_builder.finish().apply(&mut result); | 344 | let error_message = ra_fixture_after |
296 | assert_eq_text!(ra_fixture_after, &*result); | 345 | .chars() |
346 | .into_iter() | ||
347 | .skip("error:".len()) | ||
348 | .collect::<String>(); | ||
349 | assert_eq!(error_message.trim(), err.to_string()); | ||
350 | return; | ||
351 | } else { | ||
352 | panic!("Rename to '{}' failed unexpectedly: {}", new_name, err) | ||
353 | } | ||
354 | } | ||
355 | }; | ||
297 | } | 356 | } |
298 | 357 | ||
299 | fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) { | 358 | fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) { |
300 | let (analysis, position) = fixture::position(ra_fixture); | 359 | let (analysis, position) = fixture::position(ra_fixture); |
301 | let source_change = analysis.rename(position, new_name).unwrap().unwrap(); | 360 | let source_change = analysis |
361 | .rename(position, new_name) | ||
362 | .unwrap() | ||
363 | .expect("Expect returned RangeInfo to be Some, but was None"); | ||
302 | expect.assert_debug_eq(&source_change) | 364 | expect.assert_debug_eq(&source_change) |
303 | } | 365 | } |
304 | 366 | ||
@@ -313,11 +375,30 @@ mod tests { | |||
313 | } | 375 | } |
314 | 376 | ||
315 | #[test] | 377 | #[test] |
316 | fn test_rename_to_invalid_identifier() { | 378 | fn test_rename_to_invalid_identifier1() { |
317 | let (analysis, position) = fixture::position(r#"fn main() { let i<|> = 1; }"#); | 379 | check( |
318 | let new_name = "invalid!"; | 380 | "invalid!", |
319 | let source_change = analysis.rename(position, new_name).unwrap(); | 381 | r#"fn main() { let i<|> = 1; }"#, |
320 | assert!(source_change.is_none()); | 382 | "error: Invalid name `invalid!`: not an identifier", |
383 | ); | ||
384 | } | ||
385 | |||
386 | #[test] | ||
387 | fn test_rename_to_invalid_identifier2() { | ||
388 | check( | ||
389 | "multiple tokens", | ||
390 | r#"fn main() { let i<|> = 1; }"#, | ||
391 | "error: Invalid name `multiple tokens`: not an identifier", | ||
392 | ); | ||
393 | } | ||
394 | |||
395 | #[test] | ||
396 | fn test_rename_to_invalid_identifier3() { | ||
397 | check( | ||
398 | "let", | ||
399 | r#"fn main() { let i<|> = 1; }"#, | ||
400 | "error: Invalid name `let`: not an identifier", | ||
401 | ); | ||
321 | } | 402 | } |
322 | 403 | ||
323 | #[test] | 404 | #[test] |
@@ -350,6 +431,15 @@ fn main() { | |||
350 | } | 431 | } |
351 | 432 | ||
352 | #[test] | 433 | #[test] |
434 | fn test_rename_unresolved_reference() { | ||
435 | check( | ||
436 | "new_name", | ||
437 | r#"fn main() { let _ = unresolved_ref<|>; }"#, | ||
438 | "error: No references found at position", | ||
439 | ); | ||
440 | } | ||
441 | |||
442 | #[test] | ||
353 | fn test_rename_for_macro_args() { | 443 | fn test_rename_for_macro_args() { |
354 | check( | 444 | check( |
355 | "b", | 445 | "b", |