diff options
Diffstat (limited to 'crates/ide/src/references')
-rw-r--r-- | crates/ide/src/references/rename.rs | 478 |
1 files changed, 265 insertions, 213 deletions
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs index 175ddd759..1e378279d 100644 --- a/crates/ide/src/references/rename.rs +++ b/crates/ide/src/references/rename.rs | |||
@@ -14,14 +14,14 @@ use syntax::{ | |||
14 | ast::{self, NameOwner}, | 14 | ast::{self, NameOwner}, |
15 | lex_single_syntax_kind, AstNode, SyntaxKind, SyntaxNode, T, | 15 | lex_single_syntax_kind, AstNode, SyntaxKind, SyntaxNode, T, |
16 | }; | 16 | }; |
17 | use test_utils::mark; | 17 | |
18 | use text_edit::TextEdit; | 18 | use text_edit::TextEdit; |
19 | 19 | ||
20 | use crate::{display::TryToNav, FilePosition, FileSystemEdit, RangeInfo, SourceChange, TextRange}; | 20 | use crate::{display::TryToNav, FilePosition, FileSystemEdit, RangeInfo, SourceChange, TextRange}; |
21 | 21 | ||
22 | type RenameResult<T> = Result<T, RenameError>; | 22 | type RenameResult<T> = Result<T, RenameError>; |
23 | #[derive(Debug)] | 23 | #[derive(Debug)] |
24 | pub struct RenameError(pub(crate) String); | 24 | pub struct RenameError(String); |
25 | 25 | ||
26 | impl fmt::Display for RenameError { | 26 | impl fmt::Display for RenameError { |
27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
@@ -47,18 +47,26 @@ pub(crate) fn prepare_rename( | |||
47 | let sema = Semantics::new(db); | 47 | let sema = Semantics::new(db); |
48 | let source_file = sema.parse(position.file_id); | 48 | let source_file = sema.parse(position.file_id); |
49 | let syntax = source_file.syntax(); | 49 | let syntax = source_file.syntax(); |
50 | let range = match &sema | 50 | let name_like = sema |
51 | .find_node_at_offset_with_descend(&syntax, position.offset) | 51 | .find_node_at_offset_with_descend(&syntax, position.offset) |
52 | .ok_or_else(|| format_err!("No references found at position"))? | 52 | .ok_or_else(|| format_err!("No references found at position"))?; |
53 | { | 53 | let node = match &name_like { |
54 | ast::NameLike::Name(it) => it.syntax(), | 54 | ast::NameLike::Name(it) => it.syntax(), |
55 | ast::NameLike::NameRef(it) => it.syntax(), | 55 | ast::NameLike::NameRef(it) => it.syntax(), |
56 | ast::NameLike::Lifetime(it) => it.syntax(), | 56 | ast::NameLike::Lifetime(it) => it.syntax(), |
57 | } | 57 | }; |
58 | .text_range(); | 58 | Ok(RangeInfo::new(sema.original_range(node).range, ())) |
59 | Ok(RangeInfo::new(range, ())) | 59 | } |
60 | } | 60 | |
61 | 61 | // Feature: Rename | |
62 | // | ||
63 | // Renames the item below the cursor and all of its references | ||
64 | // | ||
65 | // |=== | ||
66 | // | Editor | Shortcut | ||
67 | // | ||
68 | // | VS Code | kbd:[F2] | ||
69 | // |=== | ||
62 | pub(crate) fn rename( | 70 | pub(crate) fn rename( |
63 | db: &RootDatabase, | 71 | db: &RootDatabase, |
64 | position: FilePosition, | 72 | position: FilePosition, |
@@ -79,10 +87,13 @@ pub(crate) fn rename_with_semantics( | |||
79 | let def = find_definition(sema, syntax, position)?; | 87 | let def = find_definition(sema, syntax, position)?; |
80 | match def { | 88 | match def { |
81 | Definition::ModuleDef(ModuleDef::Module(module)) => rename_mod(&sema, module, new_name), | 89 | Definition::ModuleDef(ModuleDef::Module(module)) => rename_mod(&sema, module, new_name), |
90 | Definition::SelfType(_) => bail!("Cannot rename `Self`"), | ||
91 | Definition::ModuleDef(ModuleDef::BuiltinType(_)) => bail!("Cannot rename builtin type"), | ||
82 | def => rename_reference(sema, def, new_name), | 92 | def => rename_reference(sema, def, new_name), |
83 | } | 93 | } |
84 | } | 94 | } |
85 | 95 | ||
96 | /// Called by the client when it is about to rename a file. | ||
86 | pub(crate) fn will_rename_file( | 97 | pub(crate) fn will_rename_file( |
87 | db: &RootDatabase, | 98 | db: &RootDatabase, |
88 | file_id: FileId, | 99 | file_id: FileId, |
@@ -113,7 +124,7 @@ fn check_identifier(new_name: &str) -> RenameResult<IdentifierKind> { | |||
113 | Ok(IdentifierKind::Lifetime) | 124 | Ok(IdentifierKind::Lifetime) |
114 | } | 125 | } |
115 | (SyntaxKind::LIFETIME_IDENT, _) => { | 126 | (SyntaxKind::LIFETIME_IDENT, _) => { |
116 | bail!("Invalid name `{0}`: Cannot rename lifetime to {0}", new_name) | 127 | bail!("Invalid name `{}`: not a lifetime identifier", new_name) |
117 | } | 128 | } |
118 | (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error), | 129 | (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error), |
119 | (_, None) => bail!("Invalid name `{}`: not an identifier", new_name), | 130 | (_, None) => bail!("Invalid name `{}`: not an identifier", new_name), |
@@ -153,119 +164,6 @@ fn find_definition( | |||
153 | .ok_or_else(|| format_err!("No references found at position")) | 164 | .ok_or_else(|| format_err!("No references found at position")) |
154 | } | 165 | } |
155 | 166 | ||
156 | fn source_edit_from_references( | ||
157 | _sema: &Semantics<RootDatabase>, | ||
158 | file_id: FileId, | ||
159 | references: &[FileReference], | ||
160 | def: Definition, | ||
161 | new_name: &str, | ||
162 | ) -> (FileId, TextEdit) { | ||
163 | let mut edit = TextEdit::builder(); | ||
164 | for reference in references { | ||
165 | let (range, replacement) = match &reference.name { | ||
166 | // if the ranges differ then the node is inside a macro call, we can't really attempt | ||
167 | // to make special rewrites like shorthand syntax and such, so just rename the node in | ||
168 | // the macro input | ||
169 | ast::NameLike::NameRef(name_ref) | ||
170 | if name_ref.syntax().text_range() == reference.range => | ||
171 | { | ||
172 | source_edit_from_name_ref(name_ref, new_name, def) | ||
173 | } | ||
174 | ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => { | ||
175 | source_edit_from_name(name, new_name) | ||
176 | } | ||
177 | _ => None, | ||
178 | } | ||
179 | .unwrap_or_else(|| (reference.range, new_name.to_string())); | ||
180 | edit.replace(range, replacement); | ||
181 | } | ||
182 | (file_id, edit.finish()) | ||
183 | } | ||
184 | |||
185 | fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> { | ||
186 | if let Some(_) = ast::RecordPatField::for_field_name(name) { | ||
187 | if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) { | ||
188 | return Some(( | ||
189 | TextRange::empty(ident_pat.syntax().text_range().start()), | ||
190 | format!("{}: ", new_name), | ||
191 | )); | ||
192 | } | ||
193 | } | ||
194 | None | ||
195 | } | ||
196 | |||
197 | fn source_edit_from_name_ref( | ||
198 | name_ref: &ast::NameRef, | ||
199 | new_name: &str, | ||
200 | def: Definition, | ||
201 | ) -> Option<(TextRange, String)> { | ||
202 | if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) { | ||
203 | let rcf_name_ref = record_field.name_ref(); | ||
204 | let rcf_expr = record_field.expr(); | ||
205 | match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) { | ||
206 | // field: init-expr, check if we can use a field init shorthand | ||
207 | (Some(field_name), Some(init)) => { | ||
208 | if field_name == *name_ref { | ||
209 | if init.text() == new_name { | ||
210 | mark::hit!(test_rename_field_put_init_shorthand); | ||
211 | // same names, we can use a shorthand here instead. | ||
212 | // we do not want to erase attributes hence this range start | ||
213 | let s = field_name.syntax().text_range().start(); | ||
214 | let e = record_field.syntax().text_range().end(); | ||
215 | return Some((TextRange::new(s, e), new_name.to_owned())); | ||
216 | } | ||
217 | } else if init == *name_ref { | ||
218 | if field_name.text() == new_name { | ||
219 | mark::hit!(test_rename_local_put_init_shorthand); | ||
220 | // same names, we can use a shorthand here instead. | ||
221 | // we do not want to erase attributes hence this range start | ||
222 | let s = field_name.syntax().text_range().start(); | ||
223 | let e = record_field.syntax().text_range().end(); | ||
224 | return Some((TextRange::new(s, e), new_name.to_owned())); | ||
225 | } | ||
226 | } | ||
227 | None | ||
228 | } | ||
229 | // init shorthand | ||
230 | // FIXME: instead of splitting the shorthand, recursively trigger a rename of the | ||
231 | // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547 | ||
232 | (None, Some(_)) if matches!(def, Definition::Field(_)) => { | ||
233 | mark::hit!(test_rename_field_in_field_shorthand); | ||
234 | let s = name_ref.syntax().text_range().start(); | ||
235 | Some((TextRange::empty(s), format!("{}: ", new_name))) | ||
236 | } | ||
237 | (None, Some(_)) if matches!(def, Definition::Local(_)) => { | ||
238 | mark::hit!(test_rename_local_in_field_shorthand); | ||
239 | let s = name_ref.syntax().text_range().end(); | ||
240 | Some((TextRange::empty(s), format!(": {}", new_name))) | ||
241 | } | ||
242 | _ => None, | ||
243 | } | ||
244 | } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) { | ||
245 | let rcf_name_ref = record_field.name_ref(); | ||
246 | let rcf_pat = record_field.pat(); | ||
247 | match (rcf_name_ref, rcf_pat) { | ||
248 | // field: rename | ||
249 | (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => { | ||
250 | // field name is being renamed | ||
251 | if pat.name().map_or(false, |it| it.text() == new_name) { | ||
252 | mark::hit!(test_rename_field_put_init_shorthand_pat); | ||
253 | // same names, we can use a shorthand here instead/ | ||
254 | // we do not want to erase attributes hence this range start | ||
255 | let s = field_name.syntax().text_range().start(); | ||
256 | let e = record_field.syntax().text_range().end(); | ||
257 | Some((TextRange::new(s, e), pat.to_string())) | ||
258 | } else { | ||
259 | None | ||
260 | } | ||
261 | } | ||
262 | _ => None, | ||
263 | } | ||
264 | } else { | ||
265 | None | ||
266 | } | ||
267 | } | ||
268 | |||
269 | fn rename_mod( | 167 | fn rename_mod( |
270 | sema: &Semantics<RootDatabase>, | 168 | sema: &Semantics<RootDatabase>, |
271 | module: Module, | 169 | module: Module, |
@@ -299,18 +197,77 @@ fn rename_mod( | |||
299 | TextEdit::replace(name.syntax().text_range(), new_name.to_string()), | 197 | TextEdit::replace(name.syntax().text_range(), new_name.to_string()), |
300 | ), | 198 | ), |
301 | _ => unreachable!(), | 199 | _ => unreachable!(), |
302 | }; | 200 | } |
303 | } | 201 | } |
304 | let def = Definition::ModuleDef(ModuleDef::Module(module)); | 202 | let def = Definition::ModuleDef(ModuleDef::Module(module)); |
305 | let usages = def.usages(sema).all(); | 203 | let usages = def.usages(sema).all(); |
306 | let ref_edits = usages.iter().map(|(&file_id, references)| { | 204 | let ref_edits = usages.iter().map(|(&file_id, references)| { |
307 | source_edit_from_references(sema, file_id, references, def, new_name) | 205 | (file_id, source_edit_from_references(references, def, new_name)) |
308 | }); | 206 | }); |
309 | source_change.extend(ref_edits); | 207 | source_change.extend(ref_edits); |
310 | 208 | ||
311 | Ok(source_change) | 209 | Ok(source_change) |
312 | } | 210 | } |
313 | 211 | ||
212 | fn rename_reference( | ||
213 | sema: &Semantics<RootDatabase>, | ||
214 | def: Definition, | ||
215 | new_name: &str, | ||
216 | ) -> RenameResult<SourceChange> { | ||
217 | let ident_kind = check_identifier(new_name)?; | ||
218 | |||
219 | let def_is_lbl_or_lt = matches!( | ||
220 | def, | ||
221 | Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_) | ||
222 | ); | ||
223 | match (ident_kind, def) { | ||
224 | (IdentifierKind::ToSelf, _) | ||
225 | | (IdentifierKind::Underscore, _) | ||
226 | | (IdentifierKind::Ident, _) | ||
227 | if def_is_lbl_or_lt => | ||
228 | { | ||
229 | cov_mark::hit!(rename_not_a_lifetime_ident_ref); | ||
230 | bail!("Invalid name `{}`: not a lifetime identifier", new_name) | ||
231 | } | ||
232 | (IdentifierKind::Lifetime, _) if def_is_lbl_or_lt => cov_mark::hit!(rename_lifetime), | ||
233 | (IdentifierKind::Lifetime, _) => { | ||
234 | cov_mark::hit!(rename_not_an_ident_ref); | ||
235 | bail!("Invalid name `{}`: not an identifier", new_name) | ||
236 | } | ||
237 | (IdentifierKind::ToSelf, Definition::Local(local)) if local.is_self(sema.db) => { | ||
238 | // no-op | ||
239 | cov_mark::hit!(rename_self_to_self); | ||
240 | return Ok(SourceChange::default()); | ||
241 | } | ||
242 | (ident_kind, Definition::Local(local)) if local.is_self(sema.db) => { | ||
243 | cov_mark::hit!(rename_self_to_param); | ||
244 | return rename_self_to_param(sema, local, new_name, ident_kind); | ||
245 | } | ||
246 | (IdentifierKind::ToSelf, Definition::Local(local)) => { | ||
247 | cov_mark::hit!(rename_to_self); | ||
248 | return rename_to_self(sema, local); | ||
249 | } | ||
250 | (IdentifierKind::ToSelf, _) => bail!("Invalid name `{}`: not an identifier", new_name), | ||
251 | (IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => { | ||
252 | cov_mark::hit!(rename_ident) | ||
253 | } | ||
254 | } | ||
255 | |||
256 | let usages = def.usages(sema).all(); | ||
257 | if !usages.is_empty() && ident_kind == IdentifierKind::Underscore { | ||
258 | cov_mark::hit!(rename_underscore_multiple); | ||
259 | bail!("Cannot rename reference to `_` as it is being referenced multiple times"); | ||
260 | } | ||
261 | let mut source_change = SourceChange::default(); | ||
262 | source_change.extend(usages.iter().map(|(&file_id, references)| { | ||
263 | (file_id, source_edit_from_references(&references, def, new_name)) | ||
264 | })); | ||
265 | |||
266 | let (file_id, edit) = source_edit_from_def(sema, def, new_name)?; | ||
267 | source_change.insert_source_edit(file_id, edit); | ||
268 | Ok(source_change) | ||
269 | } | ||
270 | |||
314 | fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> { | 271 | fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> { |
315 | if never!(local.is_self(sema.db)) { | 272 | if never!(local.is_self(sema.db)) { |
316 | bail!("rename_to_self invoked on self"); | 273 | bail!("rename_to_self invoked on self"); |
@@ -375,7 +332,7 @@ fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameRe | |||
375 | let usages = def.usages(sema).all(); | 332 | let usages = def.usages(sema).all(); |
376 | let mut source_change = SourceChange::default(); | 333 | let mut source_change = SourceChange::default(); |
377 | source_change.extend(usages.iter().map(|(&file_id, references)| { | 334 | source_change.extend(usages.iter().map(|(&file_id, references)| { |
378 | source_edit_from_references(sema, file_id, references, def, "self") | 335 | (file_id, source_edit_from_references(references, def, "self")) |
379 | })); | 336 | })); |
380 | source_change.insert_source_edit( | 337 | source_change.insert_source_edit( |
381 | file_id.original_file(sema.db), | 338 | file_id.original_file(sema.db), |
@@ -385,29 +342,6 @@ fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameRe | |||
385 | Ok(source_change) | 342 | Ok(source_change) |
386 | } | 343 | } |
387 | 344 | ||
388 | fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Option<TextEdit> { | ||
389 | fn target_type_name(impl_def: &ast::Impl) -> Option<String> { | ||
390 | if let Some(ast::Type::PathType(p)) = impl_def.self_ty() { | ||
391 | return Some(p.path()?.segment()?.name_ref()?.text().to_string()); | ||
392 | } | ||
393 | None | ||
394 | } | ||
395 | |||
396 | let impl_def = self_param.syntax().ancestors().find_map(|it| ast::Impl::cast(it))?; | ||
397 | let type_name = target_type_name(&impl_def)?; | ||
398 | |||
399 | let mut replacement_text = String::from(new_name); | ||
400 | replacement_text.push_str(": "); | ||
401 | match (self_param.amp_token(), self_param.mut_token()) { | ||
402 | (None, None) => (), | ||
403 | (Some(_), None) => replacement_text.push('&'), | ||
404 | (_, Some(_)) => replacement_text.push_str("&mut "), | ||
405 | }; | ||
406 | replacement_text.push_str(type_name.as_str()); | ||
407 | |||
408 | Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text)) | ||
409 | } | ||
410 | |||
411 | fn rename_self_to_param( | 345 | fn rename_self_to_param( |
412 | sema: &Semantics<RootDatabase>, | 346 | sema: &Semantics<RootDatabase>, |
413 | local: hir::Local, | 347 | local: hir::Local, |
@@ -432,66 +366,143 @@ fn rename_self_to_param( | |||
432 | let mut source_change = SourceChange::default(); | 366 | let mut source_change = SourceChange::default(); |
433 | source_change.insert_source_edit(file_id.original_file(sema.db), edit); | 367 | source_change.insert_source_edit(file_id.original_file(sema.db), edit); |
434 | source_change.extend(usages.iter().map(|(&file_id, references)| { | 368 | source_change.extend(usages.iter().map(|(&file_id, references)| { |
435 | source_edit_from_references(sema, file_id, &references, def, new_name) | 369 | (file_id, source_edit_from_references(&references, def, new_name)) |
436 | })); | 370 | })); |
437 | Ok(source_change) | 371 | Ok(source_change) |
438 | } | 372 | } |
439 | 373 | ||
440 | fn rename_reference( | 374 | fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Option<TextEdit> { |
441 | sema: &Semantics<RootDatabase>, | 375 | fn target_type_name(impl_def: &ast::Impl) -> Option<String> { |
376 | if let Some(ast::Type::PathType(p)) = impl_def.self_ty() { | ||
377 | return Some(p.path()?.segment()?.name_ref()?.text().to_string()); | ||
378 | } | ||
379 | None | ||
380 | } | ||
381 | |||
382 | let impl_def = self_param.syntax().ancestors().find_map(|it| ast::Impl::cast(it))?; | ||
383 | let type_name = target_type_name(&impl_def)?; | ||
384 | |||
385 | let mut replacement_text = String::from(new_name); | ||
386 | replacement_text.push_str(": "); | ||
387 | match (self_param.amp_token(), self_param.mut_token()) { | ||
388 | (Some(_), None) => replacement_text.push('&'), | ||
389 | (Some(_), Some(_)) => replacement_text.push_str("&mut "), | ||
390 | (_, _) => (), | ||
391 | }; | ||
392 | replacement_text.push_str(type_name.as_str()); | ||
393 | |||
394 | Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text)) | ||
395 | } | ||
396 | |||
397 | fn source_edit_from_references( | ||
398 | references: &[FileReference], | ||
442 | def: Definition, | 399 | def: Definition, |
443 | new_name: &str, | 400 | new_name: &str, |
444 | ) -> RenameResult<SourceChange> { | 401 | ) -> TextEdit { |
445 | let ident_kind = check_identifier(new_name)?; | 402 | let mut edit = TextEdit::builder(); |
446 | 403 | for reference in references { | |
447 | let def_is_lbl_or_lt = matches!( | 404 | let (range, replacement) = match &reference.name { |
448 | def, | 405 | // if the ranges differ then the node is inside a macro call, we can't really attempt |
449 | Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_) | 406 | // to make special rewrites like shorthand syntax and such, so just rename the node in |
450 | ); | 407 | // the macro input |
451 | match (ident_kind, def) { | 408 | ast::NameLike::NameRef(name_ref) |
452 | (IdentifierKind::ToSelf, _) | 409 | if name_ref.syntax().text_range() == reference.range => |
453 | | (IdentifierKind::Underscore, _) | 410 | { |
454 | | (IdentifierKind::Ident, _) | 411 | source_edit_from_name_ref(name_ref, new_name, def) |
455 | if def_is_lbl_or_lt => | 412 | } |
456 | { | 413 | ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => { |
457 | mark::hit!(rename_not_a_lifetime_ident_ref); | 414 | source_edit_from_name(name, new_name) |
458 | bail!("Invalid name `{}`: not a lifetime identifier", new_name) | 415 | } |
459 | } | 416 | _ => None, |
460 | (IdentifierKind::Lifetime, _) if def_is_lbl_or_lt => mark::hit!(rename_lifetime), | ||
461 | (IdentifierKind::Lifetime, _) => { | ||
462 | mark::hit!(rename_not_an_ident_ref); | ||
463 | bail!("Invalid name `{}`: not an identifier", new_name) | ||
464 | } | ||
465 | (IdentifierKind::ToSelf, Definition::Local(local)) if local.is_self(sema.db) => { | ||
466 | // no-op | ||
467 | mark::hit!(rename_self_to_self); | ||
468 | return Ok(SourceChange::default()); | ||
469 | } | ||
470 | (ident_kind, Definition::Local(local)) if local.is_self(sema.db) => { | ||
471 | mark::hit!(rename_self_to_param); | ||
472 | return rename_self_to_param(sema, local, new_name, ident_kind); | ||
473 | } | ||
474 | (IdentifierKind::ToSelf, Definition::Local(local)) => { | ||
475 | mark::hit!(rename_to_self); | ||
476 | return rename_to_self(sema, local); | ||
477 | } | 417 | } |
478 | (IdentifierKind::ToSelf, _) => bail!("Invalid name `{}`: not an identifier", new_name), | 418 | .unwrap_or_else(|| (reference.range, new_name.to_string())); |
479 | (IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => mark::hit!(rename_ident), | 419 | edit.replace(range, replacement); |
480 | } | 420 | } |
421 | edit.finish() | ||
422 | } | ||
481 | 423 | ||
482 | let usages = def.usages(sema).all(); | 424 | fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> { |
483 | if !usages.is_empty() && ident_kind == IdentifierKind::Underscore { | 425 | if let Some(_) = ast::RecordPatField::for_field_name(name) { |
484 | mark::hit!(rename_underscore_multiple); | 426 | if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) { |
485 | bail!("Cannot rename reference to `_` as it is being referenced multiple times"); | 427 | return Some(( |
428 | TextRange::empty(ident_pat.syntax().text_range().start()), | ||
429 | [new_name, ": "].concat(), | ||
430 | )); | ||
431 | } | ||
486 | } | 432 | } |
487 | let mut source_change = SourceChange::default(); | 433 | None |
488 | source_change.extend(usages.iter().map(|(&file_id, references)| { | 434 | } |
489 | source_edit_from_references(sema, file_id, &references, def, new_name) | ||
490 | })); | ||
491 | 435 | ||
492 | let (file_id, edit) = source_edit_from_def(sema, def, new_name)?; | 436 | fn source_edit_from_name_ref( |
493 | source_change.insert_source_edit(file_id, edit); | 437 | name_ref: &ast::NameRef, |
494 | Ok(source_change) | 438 | new_name: &str, |
439 | def: Definition, | ||
440 | ) -> Option<(TextRange, String)> { | ||
441 | if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) { | ||
442 | let rcf_name_ref = record_field.name_ref(); | ||
443 | let rcf_expr = record_field.expr(); | ||
444 | match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) { | ||
445 | // field: init-expr, check if we can use a field init shorthand | ||
446 | (Some(field_name), Some(init)) => { | ||
447 | if field_name == *name_ref { | ||
448 | if init.text() == new_name { | ||
449 | cov_mark::hit!(test_rename_field_put_init_shorthand); | ||
450 | // same names, we can use a shorthand here instead. | ||
451 | // we do not want to erase attributes hence this range start | ||
452 | let s = field_name.syntax().text_range().start(); | ||
453 | let e = record_field.syntax().text_range().end(); | ||
454 | return Some((TextRange::new(s, e), new_name.to_owned())); | ||
455 | } | ||
456 | } else if init == *name_ref { | ||
457 | if field_name.text() == new_name { | ||
458 | cov_mark::hit!(test_rename_local_put_init_shorthand); | ||
459 | // same names, we can use a shorthand here instead. | ||
460 | // we do not want to erase attributes hence this range start | ||
461 | let s = field_name.syntax().text_range().start(); | ||
462 | let e = record_field.syntax().text_range().end(); | ||
463 | return Some((TextRange::new(s, e), new_name.to_owned())); | ||
464 | } | ||
465 | } | ||
466 | None | ||
467 | } | ||
468 | // init shorthand | ||
469 | // FIXME: instead of splitting the shorthand, recursively trigger a rename of the | ||
470 | // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547 | ||
471 | (None, Some(_)) if matches!(def, Definition::Field(_)) => { | ||
472 | cov_mark::hit!(test_rename_field_in_field_shorthand); | ||
473 | let s = name_ref.syntax().text_range().start(); | ||
474 | Some((TextRange::empty(s), format!("{}: ", new_name))) | ||
475 | } | ||
476 | (None, Some(_)) if matches!(def, Definition::Local(_)) => { | ||
477 | cov_mark::hit!(test_rename_local_in_field_shorthand); | ||
478 | let s = name_ref.syntax().text_range().end(); | ||
479 | Some((TextRange::empty(s), format!(": {}", new_name))) | ||
480 | } | ||
481 | _ => None, | ||
482 | } | ||
483 | } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) { | ||
484 | let rcf_name_ref = record_field.name_ref(); | ||
485 | let rcf_pat = record_field.pat(); | ||
486 | match (rcf_name_ref, rcf_pat) { | ||
487 | // field: rename | ||
488 | (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => { | ||
489 | // field name is being renamed | ||
490 | if pat.name().map_or(false, |it| it.text() == new_name) { | ||
491 | cov_mark::hit!(test_rename_field_put_init_shorthand_pat); | ||
492 | // same names, we can use a shorthand here instead/ | ||
493 | // we do not want to erase attributes hence this range start | ||
494 | let s = field_name.syntax().text_range().start(); | ||
495 | let e = record_field.syntax().text_range().end(); | ||
496 | Some((TextRange::new(s, e), pat.to_string())) | ||
497 | } else { | ||
498 | None | ||
499 | } | ||
500 | } | ||
501 | _ => None, | ||
502 | } | ||
503 | } else { | ||
504 | None | ||
505 | } | ||
495 | } | 506 | } |
496 | 507 | ||
497 | fn source_edit_from_def( | 508 | fn source_edit_from_def( |
@@ -529,11 +540,13 @@ fn source_edit_from_def( | |||
529 | mod tests { | 540 | mod tests { |
530 | use expect_test::{expect, Expect}; | 541 | use expect_test::{expect, Expect}; |
531 | use stdx::trim_indent; | 542 | use stdx::trim_indent; |
532 | use test_utils::{assert_eq_text, mark}; | 543 | use test_utils::assert_eq_text; |
533 | use text_edit::TextEdit; | 544 | use text_edit::TextEdit; |
534 | 545 | ||
535 | use crate::{fixture, FileId}; | 546 | use crate::{fixture, FileId}; |
536 | 547 | ||
548 | use super::{RangeInfo, RenameError}; | ||
549 | |||
537 | fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | 550 | fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { |
538 | let ra_fixture_after = &trim_indent(ra_fixture_after); | 551 | let ra_fixture_after = &trim_indent(ra_fixture_after); |
539 | let (analysis, position) = fixture::position(ra_fixture_before); | 552 | let (analysis, position) = fixture::position(ra_fixture_before); |
@@ -579,6 +592,45 @@ mod tests { | |||
579 | expect.assert_debug_eq(&source_change) | 592 | expect.assert_debug_eq(&source_change) |
580 | } | 593 | } |
581 | 594 | ||
595 | fn check_prepare(ra_fixture: &str, expect: Expect) { | ||
596 | let (analysis, position) = fixture::position(ra_fixture); | ||
597 | let result = analysis | ||
598 | .prepare_rename(position) | ||
599 | .unwrap_or_else(|err| panic!("PrepareRename was cancelled: {}", err)); | ||
600 | match result { | ||
601 | Ok(RangeInfo { range, info: () }) => { | ||
602 | let source = analysis.file_text(position.file_id).unwrap(); | ||
603 | expect.assert_eq(&format!("{:?}: {}", range, &source[range])) | ||
604 | } | ||
605 | Err(RenameError(err)) => expect.assert_eq(&err), | ||
606 | }; | ||
607 | } | ||
608 | |||
609 | #[test] | ||
610 | fn test_prepare_rename_namelikes() { | ||
611 | check_prepare(r"fn name$0<'lifetime>() {}", expect![[r#"3..7: name"#]]); | ||
612 | check_prepare(r"fn name<'lifetime$0>() {}", expect![[r#"8..17: 'lifetime"#]]); | ||
613 | check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"23..27: name"#]]); | ||
614 | } | ||
615 | |||
616 | #[test] | ||
617 | fn test_prepare_rename_in_macro() { | ||
618 | check_prepare( | ||
619 | r"macro_rules! foo { | ||
620 | ($ident:ident) => { | ||
621 | pub struct $ident; | ||
622 | } | ||
623 | } | ||
624 | foo!(Foo$0);", | ||
625 | expect![[r#"83..86: Foo"#]], | ||
626 | ); | ||
627 | } | ||
628 | |||
629 | #[test] | ||
630 | fn test_prepare_rename_keyword() { | ||
631 | check_prepare(r"struct$0 Foo;", expect![[r#"No references found at position"#]]); | ||
632 | } | ||
633 | |||
582 | #[test] | 634 | #[test] |
583 | fn test_rename_to_underscore() { | 635 | fn test_rename_to_underscore() { |
584 | check("_", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let _ = 1; }"#); | 636 | check("_", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let _ = 1; }"#); |
@@ -618,7 +670,7 @@ mod tests { | |||
618 | 670 | ||
619 | #[test] | 671 | #[test] |
620 | fn test_rename_to_invalid_identifier_lifetime() { | 672 | fn test_rename_to_invalid_identifier_lifetime() { |
621 | mark::check!(rename_not_an_ident_ref); | 673 | cov_mark::check!(rename_not_an_ident_ref); |
622 | check( | 674 | check( |
623 | "'foo", | 675 | "'foo", |
624 | r#"fn main() { let i$0 = 1; }"#, | 676 | r#"fn main() { let i$0 = 1; }"#, |
@@ -628,7 +680,7 @@ mod tests { | |||
628 | 680 | ||
629 | #[test] | 681 | #[test] |
630 | fn test_rename_to_invalid_identifier_lifetime2() { | 682 | fn test_rename_to_invalid_identifier_lifetime2() { |
631 | mark::check!(rename_not_a_lifetime_ident_ref); | 683 | cov_mark::check!(rename_not_a_lifetime_ident_ref); |
632 | check( | 684 | check( |
633 | "foo", | 685 | "foo", |
634 | r#"fn main<'a>(_: &'a$0 ()) {}"#, | 686 | r#"fn main<'a>(_: &'a$0 ()) {}"#, |
@@ -638,7 +690,7 @@ mod tests { | |||
638 | 690 | ||
639 | #[test] | 691 | #[test] |
640 | fn test_rename_to_underscore_invalid() { | 692 | fn test_rename_to_underscore_invalid() { |
641 | mark::check!(rename_underscore_multiple); | 693 | cov_mark::check!(rename_underscore_multiple); |
642 | check( | 694 | check( |
643 | "_", | 695 | "_", |
644 | r#"fn main(foo$0: ()) {foo;}"#, | 696 | r#"fn main(foo$0: ()) {foo;}"#, |
@@ -657,7 +709,7 @@ mod tests { | |||
657 | 709 | ||
658 | #[test] | 710 | #[test] |
659 | fn test_rename_for_local() { | 711 | fn test_rename_for_local() { |
660 | mark::check!(rename_ident); | 712 | cov_mark::check!(rename_ident); |
661 | check( | 713 | check( |
662 | "k", | 714 | "k", |
663 | r#" | 715 | r#" |
@@ -820,7 +872,7 @@ impl Foo { | |||
820 | 872 | ||
821 | #[test] | 873 | #[test] |
822 | fn test_rename_field_in_field_shorthand() { | 874 | fn test_rename_field_in_field_shorthand() { |
823 | mark::check!(test_rename_field_in_field_shorthand); | 875 | cov_mark::check!(test_rename_field_in_field_shorthand); |
824 | check( | 876 | check( |
825 | "j", | 877 | "j", |
826 | r#" | 878 | r#" |
@@ -846,7 +898,7 @@ impl Foo { | |||
846 | 898 | ||
847 | #[test] | 899 | #[test] |
848 | fn test_rename_local_in_field_shorthand() { | 900 | fn test_rename_local_in_field_shorthand() { |
849 | mark::check!(test_rename_local_in_field_shorthand); | 901 | cov_mark::check!(test_rename_local_in_field_shorthand); |
850 | check( | 902 | check( |
851 | "j", | 903 | "j", |
852 | r#" | 904 | r#" |
@@ -1252,7 +1304,7 @@ fn foo(f: foo::Foo) { | |||
1252 | 1304 | ||
1253 | #[test] | 1305 | #[test] |
1254 | fn test_parameter_to_self() { | 1306 | fn test_parameter_to_self() { |
1255 | mark::check!(rename_to_self); | 1307 | cov_mark::check!(rename_to_self); |
1256 | check( | 1308 | check( |
1257 | "self", | 1309 | "self", |
1258 | r#" | 1310 | r#" |
@@ -1392,7 +1444,7 @@ impl Foo { | |||
1392 | 1444 | ||
1393 | #[test] | 1445 | #[test] |
1394 | fn test_owned_self_to_parameter() { | 1446 | fn test_owned_self_to_parameter() { |
1395 | mark::check!(rename_self_to_param); | 1447 | cov_mark::check!(rename_self_to_param); |
1396 | check( | 1448 | check( |
1397 | "foo", | 1449 | "foo", |
1398 | r#" | 1450 | r#" |
@@ -1445,7 +1497,7 @@ impl Foo { | |||
1445 | 1497 | ||
1446 | #[test] | 1498 | #[test] |
1447 | fn test_rename_field_put_init_shorthand() { | 1499 | fn test_rename_field_put_init_shorthand() { |
1448 | mark::check!(test_rename_field_put_init_shorthand); | 1500 | cov_mark::check!(test_rename_field_put_init_shorthand); |
1449 | check( | 1501 | check( |
1450 | "bar", | 1502 | "bar", |
1451 | r#" | 1503 | r#" |
@@ -1467,7 +1519,7 @@ fn foo(bar: i32) -> Foo { | |||
1467 | 1519 | ||
1468 | #[test] | 1520 | #[test] |
1469 | fn test_rename_local_put_init_shorthand() { | 1521 | fn test_rename_local_put_init_shorthand() { |
1470 | mark::check!(test_rename_local_put_init_shorthand); | 1522 | cov_mark::check!(test_rename_local_put_init_shorthand); |
1471 | check( | 1523 | check( |
1472 | "i", | 1524 | "i", |
1473 | r#" | 1525 | r#" |
@@ -1489,7 +1541,7 @@ fn foo(i: i32) -> Foo { | |||
1489 | 1541 | ||
1490 | #[test] | 1542 | #[test] |
1491 | fn test_struct_field_pat_into_shorthand() { | 1543 | fn test_struct_field_pat_into_shorthand() { |
1492 | mark::check!(test_rename_field_put_init_shorthand_pat); | 1544 | cov_mark::check!(test_rename_field_put_init_shorthand_pat); |
1493 | check( | 1545 | check( |
1494 | "baz", | 1546 | "baz", |
1495 | r#" | 1547 | r#" |
@@ -1601,7 +1653,7 @@ fn foo(foo: Foo) { | |||
1601 | 1653 | ||
1602 | #[test] | 1654 | #[test] |
1603 | fn test_rename_lifetimes() { | 1655 | fn test_rename_lifetimes() { |
1604 | mark::check!(rename_lifetime); | 1656 | cov_mark::check!(rename_lifetime); |
1605 | check( | 1657 | check( |
1606 | "'yeeee", | 1658 | "'yeeee", |
1607 | r#" | 1659 | r#" |
@@ -1689,7 +1741,7 @@ fn foo<'a>() -> &'a () { | |||
1689 | 1741 | ||
1690 | #[test] | 1742 | #[test] |
1691 | fn test_self_to_self() { | 1743 | fn test_self_to_self() { |
1692 | mark::check!(rename_self_to_self); | 1744 | cov_mark::check!(rename_self_to_self); |
1693 | check( | 1745 | check( |
1694 | "self", | 1746 | "self", |
1695 | r#" | 1747 | r#" |