diff options
Diffstat (limited to 'crates/ide_db')
-rw-r--r-- | crates/ide_db/src/lib.rs | 4 | ||||
-rw-r--r-- | crates/ide_db/src/rename.rs | 444 |
2 files changed, 447 insertions, 1 deletions
diff --git a/crates/ide_db/src/lib.rs b/crates/ide_db/src/lib.rs index 2ac215c06..7bbd08d6f 100644 --- a/crates/ide_db/src/lib.rs +++ b/crates/ide_db/src/lib.rs | |||
@@ -8,7 +8,6 @@ pub mod label; | |||
8 | pub mod line_index; | 8 | pub mod line_index; |
9 | pub mod symbol_index; | 9 | pub mod symbol_index; |
10 | pub mod defs; | 10 | pub mod defs; |
11 | pub mod search; | ||
12 | pub mod items_locator; | 11 | pub mod items_locator; |
13 | pub mod source_change; | 12 | pub mod source_change; |
14 | pub mod ty_filter; | 13 | pub mod ty_filter; |
@@ -16,6 +15,9 @@ pub mod traits; | |||
16 | pub mod call_info; | 15 | pub mod call_info; |
17 | pub mod helpers; | 16 | pub mod helpers; |
18 | 17 | ||
18 | pub mod search; | ||
19 | pub mod rename; | ||
20 | |||
19 | use std::{fmt, sync::Arc}; | 21 | use std::{fmt, sync::Arc}; |
20 | 22 | ||
21 | use base_db::{ | 23 | use base_db::{ |
diff --git a/crates/ide_db/src/rename.rs b/crates/ide_db/src/rename.rs new file mode 100644 index 000000000..877650df0 --- /dev/null +++ b/crates/ide_db/src/rename.rs | |||
@@ -0,0 +1,444 @@ | |||
1 | use std::fmt; | ||
2 | |||
3 | use base_db::{AnchoredPathBuf, FileId, FileRange}; | ||
4 | use either::Either; | ||
5 | use hir::{AsAssocItem, FieldSource, HasSource, InFile, ModuleSource, Semantics}; | ||
6 | use stdx::never; | ||
7 | use syntax::{ | ||
8 | ast::{self, NameOwner}, | ||
9 | lex_single_syntax_kind, AstNode, SyntaxKind, TextRange, T, | ||
10 | }; | ||
11 | use text_edit::TextEdit; | ||
12 | |||
13 | use crate::{ | ||
14 | defs::Definition, | ||
15 | search::FileReference, | ||
16 | source_change::{FileSystemEdit, SourceChange}, | ||
17 | RootDatabase, | ||
18 | }; | ||
19 | |||
20 | pub type Result<T, E = RenameError> = std::result::Result<T, E>; | ||
21 | |||
22 | #[derive(Debug)] | ||
23 | pub struct RenameError(pub String); | ||
24 | |||
25 | impl fmt::Display for RenameError { | ||
26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
27 | fmt::Display::fmt(&self.0, f) | ||
28 | } | ||
29 | } | ||
30 | |||
31 | #[macro_export] | ||
32 | macro_rules! _format_err { | ||
33 | ($fmt:expr) => { RenameError(format!($fmt)) }; | ||
34 | ($fmt:expr, $($arg:tt)+) => { RenameError(format!($fmt, $($arg)+)) } | ||
35 | } | ||
36 | pub use _format_err as format_err; | ||
37 | |||
38 | #[macro_export] | ||
39 | macro_rules! _bail { | ||
40 | ($($tokens:tt)*) => { return Err(format_err!($($tokens)*)) } | ||
41 | } | ||
42 | pub use _bail as bail; | ||
43 | |||
44 | impl Definition { | ||
45 | pub fn rename(&self, sema: &Semantics<RootDatabase>, new_name: &str) -> Result<SourceChange> { | ||
46 | match *self { | ||
47 | Definition::ModuleDef(hir::ModuleDef::Module(module)) => { | ||
48 | rename_mod(sema, module, new_name) | ||
49 | } | ||
50 | Definition::ModuleDef(hir::ModuleDef::BuiltinType(_)) => { | ||
51 | bail!("Cannot rename builtin type") | ||
52 | } | ||
53 | Definition::SelfType(_) => bail!("Cannot rename `Self`"), | ||
54 | def => rename_reference(sema, def, new_name), | ||
55 | } | ||
56 | } | ||
57 | |||
58 | /// Textual range of the identifier which will change when renaming this | ||
59 | /// `Definition`. Note that some definitions, like buitin types, can't be | ||
60 | /// renamed. | ||
61 | pub fn rename_range(self, sema: &Semantics<RootDatabase>) -> Option<FileRange> { | ||
62 | // FIXME: the `original_file_range` calls here are wrong -- they never fail, | ||
63 | // and _fall back_ to the entirety of the macro call. Such fall back is | ||
64 | // incorrect for renames. The safe behavior would be to return an error for | ||
65 | // such cases. The correct behavior would be to return an auxiliary list of | ||
66 | // "can't rename these occurrences in macros" items, and then show some kind | ||
67 | // of a dialog to the user. | ||
68 | |||
69 | let res = match self { | ||
70 | Definition::Macro(mac) => { | ||
71 | let src = mac.source(sema.db)?; | ||
72 | let name = match &src.value { | ||
73 | Either::Left(it) => it.name()?, | ||
74 | Either::Right(it) => it.name()?, | ||
75 | }; | ||
76 | src.with_value(name.syntax()).original_file_range(sema.db) | ||
77 | } | ||
78 | Definition::Field(field) => { | ||
79 | let src = field.source(sema.db)?; | ||
80 | |||
81 | match &src.value { | ||
82 | FieldSource::Named(record_field) => { | ||
83 | let name = record_field.name()?; | ||
84 | src.with_value(name.syntax()).original_file_range(sema.db) | ||
85 | } | ||
86 | FieldSource::Pos(_) => { | ||
87 | return None; | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | Definition::ModuleDef(module_def) => match module_def { | ||
92 | hir::ModuleDef::Module(module) => { | ||
93 | let src = module.declaration_source(sema.db)?; | ||
94 | let name = src.value.name()?; | ||
95 | src.with_value(name.syntax()).original_file_range(sema.db) | ||
96 | } | ||
97 | hir::ModuleDef::Function(it) => name_range(it, sema)?, | ||
98 | hir::ModuleDef::Adt(adt) => match adt { | ||
99 | hir::Adt::Struct(it) => name_range(it, sema)?, | ||
100 | hir::Adt::Union(it) => name_range(it, sema)?, | ||
101 | hir::Adt::Enum(it) => name_range(it, sema)?, | ||
102 | }, | ||
103 | hir::ModuleDef::Variant(it) => name_range(it, sema)?, | ||
104 | hir::ModuleDef::Const(it) => name_range(it, sema)?, | ||
105 | hir::ModuleDef::Static(it) => name_range(it, sema)?, | ||
106 | hir::ModuleDef::Trait(it) => name_range(it, sema)?, | ||
107 | hir::ModuleDef::TypeAlias(it) => name_range(it, sema)?, | ||
108 | hir::ModuleDef::BuiltinType(_) => return None, | ||
109 | }, | ||
110 | Definition::SelfType(_) => return None, | ||
111 | Definition::Local(local) => { | ||
112 | let src = local.source(sema.db); | ||
113 | let name = match &src.value { | ||
114 | Either::Left(bind_pat) => bind_pat.name()?, | ||
115 | Either::Right(_) => return None, | ||
116 | }; | ||
117 | src.with_value(name.syntax()).original_file_range(sema.db) | ||
118 | } | ||
119 | Definition::GenericParam(generic_param) => match generic_param { | ||
120 | hir::GenericParam::TypeParam(type_param) => { | ||
121 | let src = type_param.source(sema.db)?; | ||
122 | let name = match &src.value { | ||
123 | Either::Left(_) => return None, | ||
124 | Either::Right(type_param) => type_param.name()?, | ||
125 | }; | ||
126 | src.with_value(name.syntax()).original_file_range(sema.db) | ||
127 | } | ||
128 | hir::GenericParam::LifetimeParam(lifetime_param) => { | ||
129 | let src = lifetime_param.source(sema.db)?; | ||
130 | let lifetime = src.value.lifetime()?; | ||
131 | src.with_value(lifetime.syntax()).original_file_range(sema.db) | ||
132 | } | ||
133 | hir::GenericParam::ConstParam(it) => name_range(it, sema)?, | ||
134 | }, | ||
135 | Definition::Label(label) => { | ||
136 | let src = label.source(sema.db); | ||
137 | let lifetime = src.value.lifetime()?; | ||
138 | src.with_value(lifetime.syntax()).original_file_range(sema.db) | ||
139 | } | ||
140 | }; | ||
141 | return Some(res); | ||
142 | |||
143 | fn name_range<D>(def: D, sema: &Semantics<RootDatabase>) -> Option<FileRange> | ||
144 | where | ||
145 | D: HasSource, | ||
146 | D::Ast: ast::NameOwner, | ||
147 | { | ||
148 | let src = def.source(sema.db)?; | ||
149 | let name = src.value.name()?; | ||
150 | let res = src.with_value(name.syntax()).original_file_range(sema.db); | ||
151 | Some(res) | ||
152 | } | ||
153 | } | ||
154 | } | ||
155 | |||
156 | fn rename_mod( | ||
157 | sema: &Semantics<RootDatabase>, | ||
158 | module: hir::Module, | ||
159 | new_name: &str, | ||
160 | ) -> Result<SourceChange> { | ||
161 | if IdentifierKind::classify(new_name)? != IdentifierKind::Ident { | ||
162 | bail!("Invalid name `{0}`: cannot rename module to {0}", new_name); | ||
163 | } | ||
164 | |||
165 | let mut source_change = SourceChange::default(); | ||
166 | |||
167 | let InFile { file_id, value: def_source } = module.definition_source(sema.db); | ||
168 | let file_id = file_id.original_file(sema.db); | ||
169 | if let ModuleSource::SourceFile(..) = def_source { | ||
170 | // mod is defined in path/to/dir/mod.rs | ||
171 | let path = if module.is_mod_rs(sema.db) { | ||
172 | format!("../{}/mod.rs", new_name) | ||
173 | } else { | ||
174 | format!("{}.rs", new_name) | ||
175 | }; | ||
176 | let dst = AnchoredPathBuf { anchor: file_id, path }; | ||
177 | let move_file = FileSystemEdit::MoveFile { src: file_id, dst }; | ||
178 | source_change.push_file_system_edit(move_file); | ||
179 | } | ||
180 | |||
181 | if let Some(InFile { file_id, value: decl_source }) = module.declaration_source(sema.db) { | ||
182 | let file_id = file_id.original_file(sema.db); | ||
183 | match decl_source.name() { | ||
184 | Some(name) => source_change.insert_source_edit( | ||
185 | file_id, | ||
186 | TextEdit::replace(name.syntax().text_range(), new_name.to_string()), | ||
187 | ), | ||
188 | _ => never!("Module source node is missing a name"), | ||
189 | } | ||
190 | } | ||
191 | let def = Definition::ModuleDef(hir::ModuleDef::Module(module)); | ||
192 | let usages = def.usages(sema).all(); | ||
193 | let ref_edits = usages.iter().map(|(&file_id, references)| { | ||
194 | (file_id, source_edit_from_references(references, def, new_name)) | ||
195 | }); | ||
196 | source_change.extend(ref_edits); | ||
197 | |||
198 | Ok(source_change) | ||
199 | } | ||
200 | |||
201 | fn rename_reference( | ||
202 | sema: &Semantics<RootDatabase>, | ||
203 | mut def: Definition, | ||
204 | new_name: &str, | ||
205 | ) -> Result<SourceChange> { | ||
206 | let ident_kind = IdentifierKind::classify(new_name)?; | ||
207 | |||
208 | if matches!( | ||
209 | def, // is target a lifetime? | ||
210 | Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_) | ||
211 | ) { | ||
212 | match ident_kind { | ||
213 | IdentifierKind::Ident | IdentifierKind::Underscore => { | ||
214 | cov_mark::hit!(rename_not_a_lifetime_ident_ref); | ||
215 | bail!("Invalid name `{}`: not a lifetime identifier", new_name); | ||
216 | } | ||
217 | IdentifierKind::Lifetime => cov_mark::hit!(rename_lifetime), | ||
218 | } | ||
219 | } else { | ||
220 | match (ident_kind, def) { | ||
221 | (IdentifierKind::Lifetime, _) => { | ||
222 | cov_mark::hit!(rename_not_an_ident_ref); | ||
223 | bail!("Invalid name `{}`: not an identifier", new_name); | ||
224 | } | ||
225 | (IdentifierKind::Ident, _) => cov_mark::hit!(rename_non_local), | ||
226 | (IdentifierKind::Underscore, _) => (), | ||
227 | } | ||
228 | } | ||
229 | |||
230 | def = match def { | ||
231 | // HACK: resolve trait impl items to the item def of the trait definition | ||
232 | // so that we properly resolve all trait item references | ||
233 | Definition::ModuleDef(mod_def) => mod_def | ||
234 | .as_assoc_item(sema.db) | ||
235 | .and_then(|it| it.containing_trait_impl(sema.db)) | ||
236 | .and_then(|it| { | ||
237 | it.items(sema.db).into_iter().find_map(|it| match (it, mod_def) { | ||
238 | (hir::AssocItem::Function(trait_func), hir::ModuleDef::Function(func)) | ||
239 | if trait_func.name(sema.db) == func.name(sema.db) => | ||
240 | { | ||
241 | Some(Definition::ModuleDef(hir::ModuleDef::Function(trait_func))) | ||
242 | } | ||
243 | (hir::AssocItem::Const(trait_konst), hir::ModuleDef::Const(konst)) | ||
244 | if trait_konst.name(sema.db) == konst.name(sema.db) => | ||
245 | { | ||
246 | Some(Definition::ModuleDef(hir::ModuleDef::Const(trait_konst))) | ||
247 | } | ||
248 | ( | ||
249 | hir::AssocItem::TypeAlias(trait_type_alias), | ||
250 | hir::ModuleDef::TypeAlias(type_alias), | ||
251 | ) if trait_type_alias.name(sema.db) == type_alias.name(sema.db) => { | ||
252 | Some(Definition::ModuleDef(hir::ModuleDef::TypeAlias(trait_type_alias))) | ||
253 | } | ||
254 | _ => None, | ||
255 | }) | ||
256 | }) | ||
257 | .unwrap_or(def), | ||
258 | _ => def, | ||
259 | }; | ||
260 | let usages = def.usages(sema).all(); | ||
261 | |||
262 | if !usages.is_empty() && ident_kind == IdentifierKind::Underscore { | ||
263 | cov_mark::hit!(rename_underscore_multiple); | ||
264 | bail!("Cannot rename reference to `_` as it is being referenced multiple times"); | ||
265 | } | ||
266 | let mut source_change = SourceChange::default(); | ||
267 | source_change.extend(usages.iter().map(|(&file_id, references)| { | ||
268 | (file_id, source_edit_from_references(references, def, new_name)) | ||
269 | })); | ||
270 | |||
271 | let (file_id, edit) = source_edit_from_def(sema, def, new_name)?; | ||
272 | source_change.insert_source_edit(file_id, edit); | ||
273 | Ok(source_change) | ||
274 | } | ||
275 | |||
276 | pub fn source_edit_from_references( | ||
277 | references: &[FileReference], | ||
278 | def: Definition, | ||
279 | new_name: &str, | ||
280 | ) -> TextEdit { | ||
281 | let mut edit = TextEdit::builder(); | ||
282 | for reference in references { | ||
283 | let (range, replacement) = match &reference.name { | ||
284 | // if the ranges differ then the node is inside a macro call, we can't really attempt | ||
285 | // to make special rewrites like shorthand syntax and such, so just rename the node in | ||
286 | // the macro input | ||
287 | ast::NameLike::NameRef(name_ref) | ||
288 | if name_ref.syntax().text_range() == reference.range => | ||
289 | { | ||
290 | source_edit_from_name_ref(name_ref, new_name, def) | ||
291 | } | ||
292 | ast::NameLike::Name(name) if name.syntax().text_range() == reference.range => { | ||
293 | source_edit_from_name(name, new_name) | ||
294 | } | ||
295 | _ => None, | ||
296 | } | ||
297 | .unwrap_or_else(|| (reference.range, new_name.to_string())); | ||
298 | edit.replace(range, replacement); | ||
299 | } | ||
300 | edit.finish() | ||
301 | } | ||
302 | |||
303 | fn source_edit_from_name(name: &ast::Name, new_name: &str) -> Option<(TextRange, String)> { | ||
304 | if let Some(_) = ast::RecordPatField::for_field_name(name) { | ||
305 | if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) { | ||
306 | return Some(( | ||
307 | TextRange::empty(ident_pat.syntax().text_range().start()), | ||
308 | [new_name, ": "].concat(), | ||
309 | )); | ||
310 | } | ||
311 | } | ||
312 | None | ||
313 | } | ||
314 | |||
315 | fn source_edit_from_name_ref( | ||
316 | name_ref: &ast::NameRef, | ||
317 | new_name: &str, | ||
318 | def: Definition, | ||
319 | ) -> Option<(TextRange, String)> { | ||
320 | if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) { | ||
321 | let rcf_name_ref = record_field.name_ref(); | ||
322 | let rcf_expr = record_field.expr(); | ||
323 | match (rcf_name_ref, rcf_expr.and_then(|it| it.name_ref())) { | ||
324 | // field: init-expr, check if we can use a field init shorthand | ||
325 | (Some(field_name), Some(init)) => { | ||
326 | if field_name == *name_ref { | ||
327 | if init.text() == new_name { | ||
328 | cov_mark::hit!(test_rename_field_put_init_shorthand); | ||
329 | // same names, we can use a shorthand here instead. | ||
330 | // we do not want to erase attributes hence this range start | ||
331 | let s = field_name.syntax().text_range().start(); | ||
332 | let e = record_field.syntax().text_range().end(); | ||
333 | return Some((TextRange::new(s, e), new_name.to_owned())); | ||
334 | } | ||
335 | } else if init == *name_ref { | ||
336 | if field_name.text() == new_name { | ||
337 | cov_mark::hit!(test_rename_local_put_init_shorthand); | ||
338 | // same names, we can use a shorthand here instead. | ||
339 | // we do not want to erase attributes hence this range start | ||
340 | let s = field_name.syntax().text_range().start(); | ||
341 | let e = record_field.syntax().text_range().end(); | ||
342 | return Some((TextRange::new(s, e), new_name.to_owned())); | ||
343 | } | ||
344 | } | ||
345 | None | ||
346 | } | ||
347 | // init shorthand | ||
348 | // FIXME: instead of splitting the shorthand, recursively trigger a rename of the | ||
349 | // other name https://github.com/rust-analyzer/rust-analyzer/issues/6547 | ||
350 | (None, Some(_)) if matches!(def, Definition::Field(_)) => { | ||
351 | cov_mark::hit!(test_rename_field_in_field_shorthand); | ||
352 | let s = name_ref.syntax().text_range().start(); | ||
353 | Some((TextRange::empty(s), format!("{}: ", new_name))) | ||
354 | } | ||
355 | (None, Some(_)) if matches!(def, Definition::Local(_)) => { | ||
356 | cov_mark::hit!(test_rename_local_in_field_shorthand); | ||
357 | let s = name_ref.syntax().text_range().end(); | ||
358 | Some((TextRange::empty(s), format!(": {}", new_name))) | ||
359 | } | ||
360 | _ => None, | ||
361 | } | ||
362 | } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) { | ||
363 | let rcf_name_ref = record_field.name_ref(); | ||
364 | let rcf_pat = record_field.pat(); | ||
365 | match (rcf_name_ref, rcf_pat) { | ||
366 | // field: rename | ||
367 | (Some(field_name), Some(ast::Pat::IdentPat(pat))) if field_name == *name_ref => { | ||
368 | // field name is being renamed | ||
369 | if pat.name().map_or(false, |it| it.text() == new_name) { | ||
370 | cov_mark::hit!(test_rename_field_put_init_shorthand_pat); | ||
371 | // same names, we can use a shorthand here instead/ | ||
372 | // we do not want to erase attributes hence this range start | ||
373 | let s = field_name.syntax().text_range().start(); | ||
374 | let e = record_field.syntax().text_range().end(); | ||
375 | Some((TextRange::new(s, e), pat.to_string())) | ||
376 | } else { | ||
377 | None | ||
378 | } | ||
379 | } | ||
380 | _ => None, | ||
381 | } | ||
382 | } else { | ||
383 | None | ||
384 | } | ||
385 | } | ||
386 | |||
387 | fn source_edit_from_def( | ||
388 | sema: &Semantics<RootDatabase>, | ||
389 | def: Definition, | ||
390 | new_name: &str, | ||
391 | ) -> Result<(FileId, TextEdit)> { | ||
392 | let frange = | ||
393 | def.rename_range(sema).ok_or_else(|| format_err!("No identifier available to rename"))?; | ||
394 | |||
395 | let mut replacement_text = String::new(); | ||
396 | let mut repl_range = frange.range; | ||
397 | if let Definition::Local(local) = def { | ||
398 | if let Either::Left(pat) = local.source(sema.db).value { | ||
399 | if matches!( | ||
400 | pat.syntax().parent().and_then(ast::RecordPatField::cast), | ||
401 | Some(pat_field) if pat_field.name_ref().is_none() | ||
402 | ) { | ||
403 | replacement_text.push_str(": "); | ||
404 | replacement_text.push_str(new_name); | ||
405 | repl_range = TextRange::new( | ||
406 | pat.syntax().text_range().end(), | ||
407 | pat.syntax().text_range().end(), | ||
408 | ); | ||
409 | } | ||
410 | } | ||
411 | } | ||
412 | if replacement_text.is_empty() { | ||
413 | replacement_text.push_str(new_name); | ||
414 | } | ||
415 | let edit = TextEdit::replace(repl_range, replacement_text); | ||
416 | Ok((frange.file_id, edit)) | ||
417 | } | ||
418 | |||
419 | #[derive(Copy, Clone, Debug, PartialEq)] | ||
420 | pub enum IdentifierKind { | ||
421 | Ident, | ||
422 | Lifetime, | ||
423 | Underscore, | ||
424 | } | ||
425 | |||
426 | impl IdentifierKind { | ||
427 | pub fn classify(new_name: &str) -> Result<IdentifierKind> { | ||
428 | match lex_single_syntax_kind(new_name) { | ||
429 | Some(res) => match res { | ||
430 | (SyntaxKind::IDENT, _) => Ok(IdentifierKind::Ident), | ||
431 | (T![_], _) => Ok(IdentifierKind::Underscore), | ||
432 | (SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => { | ||
433 | Ok(IdentifierKind::Lifetime) | ||
434 | } | ||
435 | (SyntaxKind::LIFETIME_IDENT, _) => { | ||
436 | bail!("Invalid name `{}`: not a lifetime identifier", new_name) | ||
437 | } | ||
438 | (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error), | ||
439 | (_, None) => bail!("Invalid name `{}`: not an identifier", new_name), | ||
440 | }, | ||
441 | None => bail!("Invalid name `{}`: not an identifier", new_name), | ||
442 | } | ||
443 | } | ||
444 | } | ||