diff options
Diffstat (limited to 'crates/ra_ide/src/references/rename.rs')
-rw-r--r-- | crates/ra_ide/src/references/rename.rs | 329 |
1 files changed, 296 insertions, 33 deletions
diff --git a/crates/ra_ide/src/references/rename.rs b/crates/ra_ide/src/references/rename.rs index fd17bc9f2..28c6349b1 100644 --- a/crates/ra_ide/src/references/rename.rs +++ b/crates/ra_ide/src/references/rename.rs | |||
@@ -4,17 +4,18 @@ use hir::{ModuleSource, Semantics}; | |||
4 | use ra_db::{RelativePath, RelativePathBuf, SourceDatabaseExt}; | 4 | use ra_db::{RelativePath, RelativePathBuf, SourceDatabaseExt}; |
5 | use ra_ide_db::RootDatabase; | 5 | use ra_ide_db::RootDatabase; |
6 | use ra_syntax::{ | 6 | use ra_syntax::{ |
7 | algo::find_node_at_offset, ast, lex_single_valid_syntax_kind, AstNode, SyntaxKind, SyntaxNode, | 7 | algo::find_node_at_offset, ast, ast::TypeAscriptionOwner, lex_single_valid_syntax_kind, |
8 | AstNode, SyntaxKind, SyntaxNode, SyntaxToken, | ||
8 | }; | 9 | }; |
9 | use ra_text_edit::TextEdit; | 10 | use ra_text_edit::TextEdit; |
11 | use std::convert::TryInto; | ||
12 | use test_utils::mark; | ||
10 | 13 | ||
11 | use crate::{ | 14 | use crate::{ |
12 | FilePosition, FileSystemEdit, RangeInfo, Reference, ReferenceKind, SourceChange, | 15 | references::find_all_refs, FilePosition, FileSystemEdit, RangeInfo, Reference, ReferenceKind, |
13 | SourceFileEdit, TextRange, | 16 | SourceChange, SourceFileEdit, TextRange, TextSize, |
14 | }; | 17 | }; |
15 | 18 | ||
16 | use super::find_all_refs; | ||
17 | |||
18 | pub(crate) fn rename( | 19 | pub(crate) fn rename( |
19 | db: &RootDatabase, | 20 | db: &RootDatabase, |
20 | position: FilePosition, | 21 | position: FilePosition, |
@@ -22,17 +23,21 @@ pub(crate) fn rename( | |||
22 | ) -> Option<RangeInfo<SourceChange>> { | 23 | ) -> Option<RangeInfo<SourceChange>> { |
23 | match lex_single_valid_syntax_kind(new_name)? { | 24 | match lex_single_valid_syntax_kind(new_name)? { |
24 | SyntaxKind::IDENT | SyntaxKind::UNDERSCORE => (), | 25 | SyntaxKind::IDENT | SyntaxKind::UNDERSCORE => (), |
26 | SyntaxKind::SELF_KW => return rename_to_self(db, position), | ||
25 | _ => return None, | 27 | _ => return None, |
26 | } | 28 | } |
27 | 29 | ||
28 | let sema = Semantics::new(db); | 30 | let sema = Semantics::new(db); |
29 | let source_file = sema.parse(position.file_id); | 31 | let source_file = sema.parse(position.file_id); |
30 | if let Some((ast_name, ast_module)) = | 32 | let syntax = source_file.syntax(); |
31 | find_name_and_module_at_offset(source_file.syntax(), position) | 33 | if let Some((ast_name, ast_module)) = find_name_and_module_at_offset(syntax, position) { |
32 | { | ||
33 | let range = ast_name.syntax().text_range(); | 34 | let range = ast_name.syntax().text_range(); |
34 | rename_mod(&sema, &ast_name, &ast_module, position, new_name) | 35 | rename_mod(&sema, &ast_name, &ast_module, position, new_name) |
35 | .map(|info| RangeInfo::new(range, info)) | 36 | .map(|info| RangeInfo::new(range, info)) |
37 | } else if let Some(self_token) = | ||
38 | syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::SELF_KW) | ||
39 | { | ||
40 | rename_self_to_param(db, position, self_token, new_name) | ||
36 | } else { | 41 | } else { |
37 | rename_reference(sema.db, position, new_name) | 42 | rename_reference(sema.db, position, new_name) |
38 | } | 43 | } |
@@ -52,11 +57,13 @@ fn source_edit_from_reference(reference: Reference, new_name: &str) -> SourceFil | |||
52 | let file_id = reference.file_range.file_id; | 57 | let file_id = reference.file_range.file_id; |
53 | let range = match reference.kind { | 58 | let range = match reference.kind { |
54 | ReferenceKind::FieldShorthandForField => { | 59 | ReferenceKind::FieldShorthandForField => { |
60 | mark::hit!(test_rename_struct_field_for_shorthand); | ||
55 | replacement_text.push_str(new_name); | 61 | replacement_text.push_str(new_name); |
56 | replacement_text.push_str(": "); | 62 | replacement_text.push_str(": "); |
57 | TextRange::new(reference.file_range.range.start(), reference.file_range.range.start()) | 63 | TextRange::new(reference.file_range.range.start(), reference.file_range.range.start()) |
58 | } | 64 | } |
59 | ReferenceKind::FieldShorthandForLocal => { | 65 | ReferenceKind::FieldShorthandForLocal => { |
66 | mark::hit!(test_rename_local_for_field_shorthand); | ||
60 | replacement_text.push_str(": "); | 67 | replacement_text.push_str(": "); |
61 | replacement_text.push_str(new_name); | 68 | replacement_text.push_str(new_name); |
62 | TextRange::new(reference.file_range.range.end(), reference.file_range.range.end()) | 69 | TextRange::new(reference.file_range.range.end(), reference.file_range.range.end()) |
@@ -121,7 +128,113 @@ fn rename_mod( | |||
121 | source_file_edits.extend(ref_edits); | 128 | source_file_edits.extend(ref_edits); |
122 | } | 129 | } |
123 | 130 | ||
124 | Some(SourceChange::from_edits("rename", source_file_edits, file_system_edits)) | 131 | Some(SourceChange::from_edits(source_file_edits, file_system_edits)) |
132 | } | ||
133 | |||
134 | fn rename_to_self(db: &RootDatabase, position: FilePosition) -> Option<RangeInfo<SourceChange>> { | ||
135 | let sema = Semantics::new(db); | ||
136 | let source_file = sema.parse(position.file_id); | ||
137 | let syn = source_file.syntax(); | ||
138 | |||
139 | let fn_def = find_node_at_offset::<ast::FnDef>(syn, position.offset)?; | ||
140 | let params = fn_def.param_list()?; | ||
141 | if params.self_param().is_some() { | ||
142 | return None; // method already has self param | ||
143 | } | ||
144 | let first_param = params.params().next()?; | ||
145 | let mutable = match first_param.ascribed_type() { | ||
146 | Some(ast::TypeRef::ReferenceType(rt)) => rt.mut_token().is_some(), | ||
147 | _ => return None, // not renaming other types | ||
148 | }; | ||
149 | |||
150 | let RangeInfo { range, info: refs } = find_all_refs(db, position, None)?; | ||
151 | |||
152 | let param_range = first_param.syntax().text_range(); | ||
153 | let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs | ||
154 | .into_iter() | ||
155 | .partition(|reference| param_range.intersect(reference.file_range.range).is_some()); | ||
156 | |||
157 | if param_ref.is_empty() { | ||
158 | return None; | ||
159 | } | ||
160 | |||
161 | let mut edits = usages | ||
162 | .into_iter() | ||
163 | .map(|reference| source_edit_from_reference(reference, "self")) | ||
164 | .collect::<Vec<_>>(); | ||
165 | |||
166 | edits.push(SourceFileEdit { | ||
167 | file_id: position.file_id, | ||
168 | edit: TextEdit::replace( | ||
169 | param_range, | ||
170 | String::from(if mutable { "&mut self" } else { "&self" }), | ||
171 | ), | ||
172 | }); | ||
173 | |||
174 | Some(RangeInfo::new(range, SourceChange::source_file_edits(edits))) | ||
175 | } | ||
176 | |||
177 | fn text_edit_from_self_param( | ||
178 | syn: &SyntaxNode, | ||
179 | self_param: &ast::SelfParam, | ||
180 | new_name: &str, | ||
181 | ) -> Option<TextEdit> { | ||
182 | fn target_type_name(impl_def: &ast::ImplDef) -> Option<String> { | ||
183 | if let Some(ast::TypeRef::PathType(p)) = impl_def.target_type() { | ||
184 | return Some(p.path()?.segment()?.name_ref()?.text().to_string()); | ||
185 | } | ||
186 | None | ||
187 | } | ||
188 | |||
189 | let impl_def = | ||
190 | find_node_at_offset::<ast::ImplDef>(syn, self_param.syntax().text_range().start())?; | ||
191 | let type_name = target_type_name(&impl_def)?; | ||
192 | |||
193 | let mut replacement_text = String::from(new_name); | ||
194 | replacement_text.push_str(": "); | ||
195 | replacement_text.push_str(self_param.mut_token().map_or("&", |_| "&mut ")); | ||
196 | replacement_text.push_str(type_name.as_str()); | ||
197 | |||
198 | Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text)) | ||
199 | } | ||
200 | |||
201 | fn rename_self_to_param( | ||
202 | db: &RootDatabase, | ||
203 | position: FilePosition, | ||
204 | self_token: SyntaxToken, | ||
205 | new_name: &str, | ||
206 | ) -> Option<RangeInfo<SourceChange>> { | ||
207 | let sema = Semantics::new(db); | ||
208 | let source_file = sema.parse(position.file_id); | ||
209 | let syn = source_file.syntax(); | ||
210 | |||
211 | let text = db.file_text(position.file_id); | ||
212 | let fn_def = find_node_at_offset::<ast::FnDef>(syn, position.offset)?; | ||
213 | let search_range = fn_def.syntax().text_range(); | ||
214 | |||
215 | let mut edits: Vec<SourceFileEdit> = vec![]; | ||
216 | |||
217 | for (idx, _) in text.match_indices("self") { | ||
218 | let offset: TextSize = idx.try_into().unwrap(); | ||
219 | if !search_range.contains_inclusive(offset) { | ||
220 | continue; | ||
221 | } | ||
222 | if let Some(ref usage) = | ||
223 | syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW) | ||
224 | { | ||
225 | let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) { | ||
226 | text_edit_from_self_param(syn, self_param, new_name)? | ||
227 | } else { | ||
228 | TextEdit::replace(usage.text_range(), String::from(new_name)) | ||
229 | }; | ||
230 | edits.push(SourceFileEdit { file_id: position.file_id, edit }); | ||
231 | } | ||
232 | } | ||
233 | |||
234 | let range = ast::SelfParam::cast(self_token.parent()) | ||
235 | .map_or(self_token.text_range(), |p| p.syntax().text_range()); | ||
236 | |||
237 | Some(RangeInfo::new(range, SourceChange::source_file_edits(edits))) | ||
125 | } | 238 | } |
126 | 239 | ||
127 | fn rename_reference( | 240 | fn rename_reference( |
@@ -140,14 +253,14 @@ fn rename_reference( | |||
140 | return None; | 253 | return None; |
141 | } | 254 | } |
142 | 255 | ||
143 | Some(RangeInfo::new(range, SourceChange::source_file_edits("rename", edit))) | 256 | Some(RangeInfo::new(range, SourceChange::source_file_edits(edit))) |
144 | } | 257 | } |
145 | 258 | ||
146 | #[cfg(test)] | 259 | #[cfg(test)] |
147 | mod tests { | 260 | mod tests { |
148 | use insta::assert_debug_snapshot; | 261 | use insta::assert_debug_snapshot; |
149 | use ra_text_edit::TextEditBuilder; | 262 | use ra_text_edit::TextEditBuilder; |
150 | use test_utils::assert_eq_text; | 263 | use test_utils::{assert_eq_text, mark}; |
151 | 264 | ||
152 | use crate::{ | 265 | use crate::{ |
153 | mock_analysis::analysis_and_position, mock_analysis::single_file_with_position, FileId, | 266 | mock_analysis::analysis_and_position, mock_analysis::single_file_with_position, FileId, |
@@ -379,6 +492,7 @@ mod tests { | |||
379 | 492 | ||
380 | #[test] | 493 | #[test] |
381 | fn test_rename_struct_field_for_shorthand() { | 494 | fn test_rename_struct_field_for_shorthand() { |
495 | mark::check!(test_rename_struct_field_for_shorthand); | ||
382 | test_rename( | 496 | test_rename( |
383 | r#" | 497 | r#" |
384 | struct Foo { | 498 | struct Foo { |
@@ -408,6 +522,7 @@ mod tests { | |||
408 | 522 | ||
409 | #[test] | 523 | #[test] |
410 | fn test_rename_local_for_field_shorthand() { | 524 | fn test_rename_local_for_field_shorthand() { |
525 | mark::check!(test_rename_local_for_field_shorthand); | ||
411 | test_rename( | 526 | test_rename( |
412 | r#" | 527 | r#" |
413 | struct Foo { | 528 | struct Foo { |
@@ -527,17 +642,16 @@ mod tests { | |||
527 | RangeInfo { | 642 | RangeInfo { |
528 | range: 4..7, | 643 | range: 4..7, |
529 | info: SourceChange { | 644 | info: SourceChange { |
530 | label: "rename", | ||
531 | source_file_edits: [ | 645 | source_file_edits: [ |
532 | SourceFileEdit { | 646 | SourceFileEdit { |
533 | file_id: FileId( | 647 | file_id: FileId( |
534 | 2, | 648 | 2, |
535 | ), | 649 | ), |
536 | edit: TextEdit { | 650 | edit: TextEdit { |
537 | atoms: [ | 651 | indels: [ |
538 | AtomTextEdit { | 652 | Indel { |
539 | delete: 4..7, | ||
540 | insert: "foo2", | 653 | insert: "foo2", |
654 | delete: 4..7, | ||
541 | }, | 655 | }, |
542 | ], | 656 | ], |
543 | }, | 657 | }, |
@@ -554,7 +668,7 @@ mod tests { | |||
554 | dst_path: "bar/foo2.rs", | 668 | dst_path: "bar/foo2.rs", |
555 | }, | 669 | }, |
556 | ], | 670 | ], |
557 | cursor_position: None, | 671 | is_snippet: false, |
558 | }, | 672 | }, |
559 | }, | 673 | }, |
560 | ) | 674 | ) |
@@ -579,17 +693,16 @@ mod tests { | |||
579 | RangeInfo { | 693 | RangeInfo { |
580 | range: 4..7, | 694 | range: 4..7, |
581 | info: SourceChange { | 695 | info: SourceChange { |
582 | label: "rename", | ||
583 | source_file_edits: [ | 696 | source_file_edits: [ |
584 | SourceFileEdit { | 697 | SourceFileEdit { |
585 | file_id: FileId( | 698 | file_id: FileId( |
586 | 1, | 699 | 1, |
587 | ), | 700 | ), |
588 | edit: TextEdit { | 701 | edit: TextEdit { |
589 | atoms: [ | 702 | indels: [ |
590 | AtomTextEdit { | 703 | Indel { |
591 | delete: 4..7, | ||
592 | insert: "foo2", | 704 | insert: "foo2", |
705 | delete: 4..7, | ||
593 | }, | 706 | }, |
594 | ], | 707 | ], |
595 | }, | 708 | }, |
@@ -606,7 +719,7 @@ mod tests { | |||
606 | dst_path: "foo2/mod.rs", | 719 | dst_path: "foo2/mod.rs", |
607 | }, | 720 | }, |
608 | ], | 721 | ], |
609 | cursor_position: None, | 722 | is_snippet: false, |
610 | }, | 723 | }, |
611 | }, | 724 | }, |
612 | ) | 725 | ) |
@@ -662,17 +775,16 @@ mod tests { | |||
662 | RangeInfo { | 775 | RangeInfo { |
663 | range: 8..11, | 776 | range: 8..11, |
664 | info: SourceChange { | 777 | info: SourceChange { |
665 | label: "rename", | ||
666 | source_file_edits: [ | 778 | source_file_edits: [ |
667 | SourceFileEdit { | 779 | SourceFileEdit { |
668 | file_id: FileId( | 780 | file_id: FileId( |
669 | 2, | 781 | 2, |
670 | ), | 782 | ), |
671 | edit: TextEdit { | 783 | edit: TextEdit { |
672 | atoms: [ | 784 | indels: [ |
673 | AtomTextEdit { | 785 | Indel { |
674 | delete: 8..11, | ||
675 | insert: "foo2", | 786 | insert: "foo2", |
787 | delete: 8..11, | ||
676 | }, | 788 | }, |
677 | ], | 789 | ], |
678 | }, | 790 | }, |
@@ -682,10 +794,10 @@ mod tests { | |||
682 | 1, | 794 | 1, |
683 | ), | 795 | ), |
684 | edit: TextEdit { | 796 | edit: TextEdit { |
685 | atoms: [ | 797 | indels: [ |
686 | AtomTextEdit { | 798 | Indel { |
687 | delete: 27..30, | ||
688 | insert: "foo2", | 799 | insert: "foo2", |
800 | delete: 27..30, | ||
689 | }, | 801 | }, |
690 | ], | 802 | ], |
691 | }, | 803 | }, |
@@ -702,13 +814,164 @@ mod tests { | |||
702 | dst_path: "bar/foo2.rs", | 814 | dst_path: "bar/foo2.rs", |
703 | }, | 815 | }, |
704 | ], | 816 | ], |
705 | cursor_position: None, | 817 | is_snippet: false, |
706 | }, | 818 | }, |
707 | }, | 819 | }, |
708 | ) | 820 | ) |
709 | "###); | 821 | "###); |
710 | } | 822 | } |
711 | 823 | ||
824 | #[test] | ||
825 | fn test_enum_variant_from_module_1() { | ||
826 | test_rename( | ||
827 | r#" | ||
828 | mod foo { | ||
829 | pub enum Foo { | ||
830 | Bar<|>, | ||
831 | } | ||
832 | } | ||
833 | |||
834 | fn func(f: foo::Foo) { | ||
835 | match f { | ||
836 | foo::Foo::Bar => {} | ||
837 | } | ||
838 | } | ||
839 | "#, | ||
840 | "Baz", | ||
841 | r#" | ||
842 | mod foo { | ||
843 | pub enum Foo { | ||
844 | Baz, | ||
845 | } | ||
846 | } | ||
847 | |||
848 | fn func(f: foo::Foo) { | ||
849 | match f { | ||
850 | foo::Foo::Baz => {} | ||
851 | } | ||
852 | } | ||
853 | "#, | ||
854 | ); | ||
855 | } | ||
856 | |||
857 | #[test] | ||
858 | fn test_enum_variant_from_module_2() { | ||
859 | test_rename( | ||
860 | r#" | ||
861 | mod foo { | ||
862 | pub struct Foo { | ||
863 | pub bar<|>: uint, | ||
864 | } | ||
865 | } | ||
866 | |||
867 | fn foo(f: foo::Foo) { | ||
868 | let _ = f.bar; | ||
869 | } | ||
870 | "#, | ||
871 | "baz", | ||
872 | r#" | ||
873 | mod foo { | ||
874 | pub struct Foo { | ||
875 | pub baz: uint, | ||
876 | } | ||
877 | } | ||
878 | |||
879 | fn foo(f: foo::Foo) { | ||
880 | let _ = f.baz; | ||
881 | } | ||
882 | "#, | ||
883 | ); | ||
884 | } | ||
885 | |||
886 | #[test] | ||
887 | fn test_parameter_to_self() { | ||
888 | test_rename( | ||
889 | r#" | ||
890 | struct Foo { | ||
891 | i: i32, | ||
892 | } | ||
893 | |||
894 | impl Foo { | ||
895 | fn f(foo<|>: &mut Foo) -> i32 { | ||
896 | foo.i | ||
897 | } | ||
898 | } | ||
899 | "#, | ||
900 | "self", | ||
901 | r#" | ||
902 | struct Foo { | ||
903 | i: i32, | ||
904 | } | ||
905 | |||
906 | impl Foo { | ||
907 | fn f(&mut self) -> i32 { | ||
908 | self.i | ||
909 | } | ||
910 | } | ||
911 | "#, | ||
912 | ); | ||
913 | } | ||
914 | |||
915 | #[test] | ||
916 | fn test_self_to_parameter() { | ||
917 | test_rename( | ||
918 | r#" | ||
919 | struct Foo { | ||
920 | i: i32, | ||
921 | } | ||
922 | |||
923 | impl Foo { | ||
924 | fn f(&mut <|>self) -> i32 { | ||
925 | self.i | ||
926 | } | ||
927 | } | ||
928 | "#, | ||
929 | "foo", | ||
930 | r#" | ||
931 | struct Foo { | ||
932 | i: i32, | ||
933 | } | ||
934 | |||
935 | impl Foo { | ||
936 | fn f(foo: &mut Foo) -> i32 { | ||
937 | foo.i | ||
938 | } | ||
939 | } | ||
940 | "#, | ||
941 | ); | ||
942 | } | ||
943 | |||
944 | #[test] | ||
945 | fn test_self_in_path_to_parameter() { | ||
946 | test_rename( | ||
947 | r#" | ||
948 | struct Foo { | ||
949 | i: i32, | ||
950 | } | ||
951 | |||
952 | impl Foo { | ||
953 | fn f(&self) -> i32 { | ||
954 | let self_var = 1; | ||
955 | self<|>.i | ||
956 | } | ||
957 | } | ||
958 | "#, | ||
959 | "foo", | ||
960 | r#" | ||
961 | struct Foo { | ||
962 | i: i32, | ||
963 | } | ||
964 | |||
965 | impl Foo { | ||
966 | fn f(foo: &Foo) -> i32 { | ||
967 | let self_var = 1; | ||
968 | foo.i | ||
969 | } | ||
970 | } | ||
971 | "#, | ||
972 | ); | ||
973 | } | ||
974 | |||
712 | fn test_rename(text: &str, new_name: &str, expected: &str) { | 975 | fn test_rename(text: &str, new_name: &str, expected: &str) { |
713 | let (analysis, position) = single_file_with_position(text); | 976 | let (analysis, position) = single_file_with_position(text); |
714 | let source_change = analysis.rename(position, new_name).unwrap(); | 977 | let source_change = analysis.rename(position, new_name).unwrap(); |
@@ -717,13 +980,13 @@ mod tests { | |||
717 | if let Some(change) = source_change { | 980 | if let Some(change) = source_change { |
718 | for edit in change.info.source_file_edits { | 981 | for edit in change.info.source_file_edits { |
719 | file_id = Some(edit.file_id); | 982 | file_id = Some(edit.file_id); |
720 | for atom in edit.edit.as_atoms() { | 983 | for indel in edit.edit.into_iter() { |
721 | text_edit_builder.replace(atom.delete, atom.insert.clone()); | 984 | text_edit_builder.replace(indel.delete, indel.insert); |
722 | } | 985 | } |
723 | } | 986 | } |
724 | } | 987 | } |
725 | let result = | 988 | let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string(); |
726 | text_edit_builder.finish().apply(&*analysis.file_text(file_id.unwrap()).unwrap()); | 989 | text_edit_builder.finish().apply(&mut result); |
727 | assert_eq_text!(expected, &*result); | 990 | assert_eq_text!(expected, &*result); |
728 | } | 991 | } |
729 | } | 992 | } |