diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs index caa6700de9f9..9dcaa68098bc 100644 --- a/crates/hir/src/semantics.rs +++ b/crates/hir/src/semantics.rs @@ -121,6 +121,9 @@ impl PathResolutionPerNs { pub fn any(&self) -> Option { self.type_ns.or(self.value_ns).or(self.macro_ns) } + pub fn to_small_vec(&self) -> SmallVec<[Option; 3]> { + smallvec![self.type_ns, self.value_ns, self.macro_ns,] + } } #[derive(Debug)] diff --git a/crates/ide-db/src/rename.rs b/crates/ide-db/src/rename.rs index fa2a46a0f7c2..1b051d177218 100644 --- a/crates/ide-db/src/rename.rs +++ b/crates/ide-db/src/rename.rs @@ -29,6 +29,7 @@ use crate::{ use base_db::AnchoredPathBuf; use either::Either; use hir::{EditionedFileId, FieldSource, FileRange, InFile, ModuleSource, Semantics}; +use itertools::Itertools; use span::{Edition, FileId, SyntaxContext}; use stdx::{TupleExt, never}; use syntax::{ @@ -316,7 +317,7 @@ fn rename_mod( let ref_edits = usages.iter().map(|(file_id, references)| { ( file_id.file_id(sema.db), - source_edit_from_references(references, def, new_name, file_id.edition(sema.db)), + source_edit_from_references(sema, references, def, new_name, file_id.edition(sema.db)), ) }); source_change.extend(ref_edits); @@ -363,7 +364,7 @@ fn rename_reference( source_change.extend(usages.iter().map(|(file_id, references)| { ( file_id.file_id(sema.db), - source_edit_from_references(references, def, new_name, file_id.edition(sema.db)), + source_edit_from_references(sema, references, def, new_name, file_id.edition(sema.db)), ) })); @@ -375,6 +376,7 @@ fn rename_reference( } pub fn source_edit_from_references( + sema: &Semantics<'_, RootDatabase>, references: &[FileReference], def: Definition, new_name: &str, @@ -395,7 +397,7 @@ pub fn source_edit_from_references( // to make special rewrites like shorthand syntax and such, so just rename the node in // the macro input FileReferenceNode::NameRef(name_ref) if name_range == range => { - source_edit_from_name_ref(&mut edit, name_ref, &new_name, def) + source_edit_from_name_ref(&mut edit, sema, name_ref, &new_name, def) } FileReferenceNode::Name(name) if name_range == range => { source_edit_from_name(&mut edit, name, &new_name) @@ -438,6 +440,7 @@ fn source_edit_from_name(edit: &mut TextEditBuilder, name: &ast::Name, new_name: fn source_edit_from_name_ref( edit: &mut TextEditBuilder, + sema: &Semantics<'_, RootDatabase>, name_ref: &ast::NameRef, new_name: &str, def: Definition, @@ -525,6 +528,31 @@ fn source_edit_from_name_ref( } _ => (), } + } else if let Some(res) = ast::UseTree::find_tail_use_tree_for_name_ref(name_ref) + .and_then(|u| u.path()) + .and_then(|p| sema.resolve_path_per_ns(&p)) + { + let res = res + .to_small_vec() + .into_iter() + .flatten() + .filter_map(|res| match res { + hir::PathResolution::Def(def) => Some(Definition::from(def)), + _ => None, + }) + .unique() + .collect::>(); + + let range = name_ref.syntax().text_range(); + if res.iter().any(|res| res == &def) { + if res.len() == 1 { + edit.replace(range, new_name.to_owned()); + } else { + edit.replace(range, format!("{{{}, {}}}", new_name, name_ref.text())); + } + } + + return true; } false } diff --git a/crates/ide-db/src/search.rs b/crates/ide-db/src/search.rs index 30be5bc21b49..35b69d407fda 100644 --- a/crates/ide-db/src/search.rs +++ b/crates/ide-db/src/search.rs @@ -1170,6 +1170,22 @@ impl<'a> FindUsages<'a> { } } + fn is_related_import(&self, name_ref: &ast::NameRef) -> bool { + ast::UseTree::find_tail_use_tree_for_name_ref(name_ref) + .and_then(|u| u.path()) + .and_then(|path| self.sema.resolve_path_per_ns(&path)) + .map(|res| { + res.to_small_vec() + .into_iter() + .filter_map(|res| match res { + Some(PathResolution::Def(def)) => Some(Definition::from(def)), + _ => None, + }) + .any(|def| def == self.def) + }) + == Some(true) + } + fn found_name_ref( &self, name_ref: &ast::NameRef, @@ -1180,7 +1196,8 @@ impl<'a> FindUsages<'a> { if self.def == def // is our def a trait assoc item? then we want to find all assoc items from trait impls of our trait || matches!(self.assoc_item_container, Some(hir::AssocItemContainer::Trait(_))) - && convert_to_def_in_trait(self.sema.db, def) == self.def => + && convert_to_def_in_trait(self.sema.db, def) == self.def + || self.is_related_import(name_ref) => { let FileRange { file_id, range } = self.sema.original_range(name_ref.syntax()); let reference = FileReference { diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 4fa116444b7f..d55ef9ac24c9 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -1762,7 +1762,6 @@ trait Trait { ) } - // FIXME: import is classified as function #[test] fn attr() { check( @@ -1776,6 +1775,7 @@ fn func() {} expect![[r#" identity Attribute FileId(1) 1..107 32..40 + FileId(0) 17..25 import FileId(0) 43..51 "#]], ); @@ -1793,7 +1793,6 @@ fn func$0() {} ); } - // FIXME: import is classified as function #[test] fn proc_macro() { check( @@ -1806,6 +1805,7 @@ mirror$0! {} expect![[r#" mirror ProcMacro FileId(1) 1..77 22..28 + FileId(0) 17..23 import FileId(0) 26..32 "#]], ) diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index e6cda60cd95b..70e455b088fd 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -4,7 +4,7 @@ //! tests. This module also implements a couple of magic tricks, like renaming //! `self` and to `self` (to switch between associated function and method). -use hir::{AsAssocItem, InFile, Semantics}; +use hir::{AsAssocItem, InFile, PathResolution, Semantics}; use ide_db::{ FileId, FileRange, RootDatabase, defs::{Definition, NameClass, NameRefClass}, @@ -120,7 +120,13 @@ pub(crate) fn rename( source_change.extend(usages.references.get_mut(&file_id).iter().map(|refs| { ( position.file_id, - source_edit_from_references(refs, def, new_name, file_id.edition(db)), + source_edit_from_references( + &sema, + refs, + def, + new_name, + file_id.edition(db), + ), ) })); @@ -245,29 +251,47 @@ fn find_definitions( }) .ok_or_else(|| format_err!("No references found at position")), ast::NameLike::NameRef(name_ref) => { - NameRefClass::classify(sema, name_ref) - .map(|class| match class { - NameRefClass::Definition(def, _) => def, - NameRefClass::FieldShorthand { local_ref, field_ref: _, adt_subst: _ } => { - Definition::Local(local_ref) - } - NameRefClass::ExternCrateShorthand { decl, .. } => { - Definition::ExternCrateDecl(decl) - } - }) - // FIXME: uncomment this once we resolve to usages to extern crate declarations - .filter(|def| !matches!(def, Definition::ExternCrateDecl(..))) - .ok_or_else(|| format_err!("No references found at position")) - .and_then(|def| { - // if the name differs from the definitions name it has to be an alias - if def - .name(sema.db).is_some_and(|it| it.as_str() != name_ref.text().trim_start_matches("r#")) - { - Err(format_err!("Renaming aliases is currently unsupported")) - } else { - Ok(def) + if let Some(path) = ast::UseTree::find_tail_use_tree_for_name_ref(name_ref).and_then(|u| u.path()).and_then(|p| sema.resolve_path_per_ns(&p)){ + let defs = path.to_small_vec().into_iter().filter_map(|res| match res{ + Some(PathResolution::Def(def)) => Some(Definition::from(def)), + _ => None, + }).unique().collect::>(); + match defs.len() { + 0 => Err(format_err!("No references found at position")), + 1 => { + if defs[0].name(sema.db).is_some_and(|it| it.as_str() != name_ref.text().trim_start_matches("r#")) { + Err(format_err!("Renaming aliases is currently unsupported")) + } else { + Ok(defs[0]) + } } - }) + 2.. => Err(format_err!("Ambiguous import is currently unsupported")), + } + } else { + NameRefClass::classify(sema, name_ref) + .map(|class| match class { + NameRefClass::Definition(def, _) => def, + NameRefClass::FieldShorthand { local_ref, field_ref: _, adt_subst: _ } => { + Definition::Local(local_ref) + } + NameRefClass::ExternCrateShorthand { decl, .. } => { + Definition::ExternCrateDecl(decl) + } + }) + // FIXME: uncomment this once we resolve to usages to extern crate declarations + .filter(|def| !matches!(def, Definition::ExternCrateDecl(..))) + .ok_or_else(|| format_err!("No references found at position")) + .and_then(|def| { + // if the name differs from the definitions name it has to be an alias + if def + .name(sema.db).is_some_and(|it| it.as_str() != name_ref.text().trim_start_matches("r#")) + { + Err(format_err!("Renaming aliases is currently unsupported")) + } else { + Ok(def) + } + }) + } } ast::NameLike::Lifetime(lifetime) => { NameRefClass::classify_lifetime(sema, lifetime) @@ -370,7 +394,7 @@ fn rename_to_self( source_change.extend(usages.iter().map(|(file_id, references)| { ( file_id.file_id(sema.db), - source_edit_from_references(references, def, "self", file_id.edition(sema.db)), + source_edit_from_references(sema, references, def, "self", file_id.edition(sema.db)), ) })); source_change.insert_source_edit( @@ -409,7 +433,7 @@ fn rename_self_to_param( source_change.extend(usages.iter().map(|(file_id, references)| { ( file_id.file_id(sema.db), - source_edit_from_references(references, def, new_name, file_id.edition(sema.db)), + source_edit_from_references(sema, references, def, new_name, file_id.edition(sema.db)), ) })); Ok(source_change) @@ -3262,4 +3286,150 @@ trait Trait { "#, ); } + + #[test] + fn rename_conflicts_in_name_space() { + check( + "qux", + r#" +use foo::bar; + +mod foo { + pub mod bar$0 {} + pub fn bar() {} +} +"#, + r#" +use foo::{qux, bar}; + +mod foo { + pub mod qux {} + pub fn bar() {} +} +"#, + ); + + check( + "qux", + r#" +use foo::{bar, baz}; + +mod foo { + pub mod bar$0 {} + pub fn bar() {} + pub fn baz() {} +} +"#, + r#" +use foo::{{qux, bar}, baz}; + +mod foo { + pub mod qux {} + pub fn bar() {} + pub fn baz() {} +} +"#, + ); + } + + #[test] + fn rename_conflicts_value_in_name_space() { + check( + "qux", + r#" +use foo::bar; + +mod foo { + pub mod bar {} + pub fn bar$0() {} +} +"#, + r#" +use foo::{qux, bar}; + +mod foo { + pub mod bar {} + pub fn qux() {} +} +"#, + ); + } + + #[test] + fn rename_conflicts_struct_fn_in_name_space() { + check( + "Renamed", + r#" +pub use aligned::Aligned; + +mod aligned { + pub struct Aligned$0 { + size: usize, + } + pub fn Aligned(size: usize) -> Aligned { + Aligned { size } + } +} +"#, + r#" +pub use aligned::{Renamed, Aligned}; + +mod aligned { + pub struct Renamed { + size: usize, + } + pub fn Aligned(size: usize) -> Renamed { + Renamed { size } + } +} +"#, + ); + + check( + "Renamed", + r#" +pub use aligned::Aligned; + +mod aligned { + pub struct Aligned { + size: usize, + } + pub fn Aligned$0(size: usize) -> Aligned { + Aligned { size } + } +} +"#, + r#" +pub use aligned::{Renamed, Aligned}; + +mod aligned { + pub struct Aligned { + size: usize, + } + pub fn Renamed(size: usize) -> Aligned { + Aligned { size } + } +} +"#, + ); + } + + #[test] + fn disallow_ambiguous_import() { + check_prepare( + r#" +pub use aligned::Aligned$0; + +mod aligned { + pub struct Aligned { + size: usize, + } + pub fn Aligned(size: usize) -> Aligned { + Aligned { size } + } +} +"#, + expect!["Ambiguous import is currently unsupported"], + ); + } } diff --git a/crates/syntax/src/ast/node_ext.rs b/crates/syntax/src/ast/node_ext.rs index b9ccd34cff06..c9e64c31a827 100644 --- a/crates/syntax/src/ast/node_ext.rs +++ b/crates/syntax/src/ast/node_ext.rs @@ -370,6 +370,18 @@ impl ast::UseTree { } this } + + pub fn find_tail_use_tree_for_name_ref(name_ref: &ast::NameRef) -> Option { + let use_tree = name_ref + .syntax() + .ancestors() + .find_map(ast::UseTree::cast) + .filter(|u| u.use_tree_list().is_none()); + + let path = use_tree.clone()?.syntax().first_child().and_then(ast::Path::cast)?; + let last_path = path.segments().last().and_then(|it| it.name_ref())?; + if last_path == *name_ref { use_tree } else { None } + } } impl ast::UseTreeList {