diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index e3a332d30a..d5a3d9034c 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -1114,6 +1114,7 @@ pub enum AssocItem { Const(Const), TypeAlias(TypeAlias), } +#[derive(Debug)] pub enum AssocItemContainer { Trait(Trait), Impl(Impl), @@ -2136,6 +2137,16 @@ impl ScopeDef { } } +impl From for ScopeDef { + fn from(item: ItemInNs) -> Self { + match item { + ItemInNs::Types(id) => ScopeDef::ModuleDef(id.into()), + ItemInNs::Values(id) => ScopeDef::ModuleDef(id.into()), + ItemInNs::Macros(id) => ScopeDef::MacroDef(id.into()), + } + } +} + pub trait HasVisibility { fn visibility(&self, db: &dyn HirDatabase) -> Visibility; fn is_visible_from(&self, db: &dyn HirDatabase, module: Module) -> bool { diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index b600178ee1..f83ed65d5a 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -478,7 +478,6 @@ impl Analysis { position: FilePosition, full_import_path: &str, imported_name: String, - import_for_trait_assoc_item: bool, ) -> Cancelable> { Ok(self .with_db(|db| { @@ -488,7 +487,6 @@ impl Analysis { position, full_import_path, imported_name, - import_for_trait_assoc_item, ) })? .unwrap_or_default()) diff --git a/crates/ide_assists/src/handlers/auto_import.rs b/crates/ide_assists/src/handlers/auto_import.rs index 1422224ac0..7caee8df04 100644 --- a/crates/ide_assists/src/handlers/auto_import.rs +++ b/crates/ide_assists/src/handlers/auto_import.rs @@ -1,7 +1,7 @@ use ide_db::helpers::{ import_assets::{ImportAssets, ImportCandidate}, insert_use::{insert_use, ImportScope}, - mod_path_to_ast, + item_name, mod_path_to_ast, }; use syntax::{ast, AstNode, SyntaxNode}; @@ -92,14 +92,19 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()> let range = ctx.sema.original_range(&syntax_under_caret).range; let group = import_group_message(import_assets.import_candidate()); let scope = ImportScope::find_insert_use_container(&syntax_under_caret, &ctx.sema)?; - for (import, _) in proposed_imports { + for import in proposed_imports { + let name = match item_name(ctx.db(), import.original_item) { + Some(name) => name, + None => continue, + }; acc.add_group( &group, AssistId("auto_import", AssistKind::QuickFix), - format!("Import `{}`", &import), + format!("Import `{}`", name), range, |builder| { - let rewriter = insert_use(&scope, mod_path_to_ast(&import), ctx.config.insert_use); + let rewriter = + insert_use(&scope, mod_path_to_ast(&import.import_path), ctx.config.insert_use); builder.rewrite(rewriter); }, ); @@ -125,10 +130,10 @@ fn import_group_message(import_candidate: &ImportCandidate) -> GroupLabel { let name = match import_candidate { ImportCandidate::Path(candidate) => format!("Import {}", candidate.name.text()), ImportCandidate::TraitAssocItem(candidate) => { - format!("Import a trait for item {}", candidate.name.text()) + format!("Import a trait for item {}", candidate.assoc_item_name.text()) } ImportCandidate::TraitMethod(candidate) => { - format!("Import a trait for method {}", candidate.name.text()) + format!("Import a trait for method {}", candidate.assoc_item_name.text()) } }; GroupLabel(name) @@ -220,41 +225,6 @@ mod tests { ); } - #[test] - fn auto_imports_are_merged() { - check_assist( - auto_import, - r" - use PubMod::PubStruct1; - - struct Test { - test: Pub$0Struct2, - } - - pub mod PubMod { - pub struct PubStruct1; - pub struct PubStruct2 { - _t: T, - } - } - ", - r" - use PubMod::{PubStruct1, PubStruct2}; - - struct Test { - test: PubStruct2, - } - - pub mod PubMod { - pub struct PubStruct1; - pub struct PubStruct2 { - _t: T, - } - } - ", - ); - } - #[test] fn applicable_when_found_multiple_imports() { check_assist( diff --git a/crates/ide_assists/src/handlers/qualify_path.rs b/crates/ide_assists/src/handlers/qualify_path.rs index d3e34e5403..272874ae39 100644 --- a/crates/ide_assists/src/handlers/qualify_path.rs +++ b/crates/ide_assists/src/handlers/qualify_path.rs @@ -1,7 +1,10 @@ use std::iter; use hir::AsAssocItem; -use ide_db::helpers::{import_assets::ImportCandidate, mod_path_to_ast}; +use ide_db::helpers::{ + import_assets::{ImportCandidate, LocatedImport}, + item_name, mod_path_to_ast, +}; use ide_db::RootDatabase; use syntax::{ ast, @@ -71,17 +74,17 @@ pub(crate) fn qualify_path(acc: &mut Assists, ctx: &AssistContext) -> Option<()> }; let group_label = group_label(candidate); - for (import, item) in proposed_imports { + for import in proposed_imports { acc.add_group( &group_label, AssistId("qualify_path", AssistKind::QuickFix), - label(candidate, &import), + label(ctx.db(), candidate, &import), range, |builder| { qualify_candidate.qualify( |replace_with: String| builder.replace(range, replace_with), - import, - item, + &import.import_path, + import.item_to_import, ) }, ); @@ -97,8 +100,13 @@ enum QualifyCandidate<'db> { } impl QualifyCandidate<'_> { - fn qualify(&self, mut replacer: impl FnMut(String), import: hir::ModPath, item: hir::ItemInNs) { - let import = mod_path_to_ast(&import); + fn qualify( + &self, + mut replacer: impl FnMut(String), + import: &hir::ModPath, + item: hir::ItemInNs, + ) { + let import = mod_path_to_ast(import); match self { QualifyCandidate::QualifierStart(segment, generics) => { let generics = generics.as_ref().map_or_else(String::new, ToString::to_string); @@ -183,23 +191,29 @@ fn item_as_trait(db: &RootDatabase, item: hir::ItemInNs) -> Option { fn group_label(candidate: &ImportCandidate) -> GroupLabel { let name = match candidate { ImportCandidate::Path(it) => &it.name, - ImportCandidate::TraitAssocItem(it) | ImportCandidate::TraitMethod(it) => &it.name, + ImportCandidate::TraitAssocItem(it) | ImportCandidate::TraitMethod(it) => { + &it.assoc_item_name + } } .text(); GroupLabel(format!("Qualify {}", name)) } -fn label(candidate: &ImportCandidate, import: &hir::ModPath) -> String { +fn label(db: &RootDatabase, candidate: &ImportCandidate, import: &LocatedImport) -> String { + let display_path = match item_name(db, import.original_item) { + Some(display_path) => display_path.to_string(), + None => "{unknown}".to_string(), + }; match candidate { ImportCandidate::Path(candidate) => { if candidate.qualifier.is_some() { - format!("Qualify with `{}`", &import) + format!("Qualify with `{}`", display_path) } else { - format!("Qualify as `{}`", &import) + format!("Qualify as `{}`", display_path) } } - ImportCandidate::TraitAssocItem(_) => format!("Qualify `{}`", &import), - ImportCandidate::TraitMethod(_) => format!("Qualify with cast as `{}`", &import), + ImportCandidate::TraitAssocItem(_) => format!("Qualify `{}`", display_path), + ImportCandidate::TraitMethod(_) => format!("Qualify with cast as `{}`", display_path), } } diff --git a/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs b/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs index c69bc5cacd..88fe2fe904 100644 --- a/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs +++ b/crates/ide_assists/src/handlers/replace_derive_with_manual_impl.rs @@ -1,5 +1,6 @@ +use hir::ModuleDef; use ide_db::helpers::mod_path_to_ast; -use ide_db::imports_locator; +use ide_db::items_locator; use itertools::Itertools; use syntax::{ ast::{self, make, AstNode, NameOwner}, @@ -64,22 +65,20 @@ pub(crate) fn replace_derive_with_manual_impl( let current_module = ctx.sema.scope(annotated_name.syntax()).module()?; let current_crate = current_module.krate(); - let found_traits = imports_locator::find_exact_imports( - &ctx.sema, - current_crate, - trait_token.text().to_string(), - ) - .filter_map(|candidate: either::Either| match candidate { - either::Either::Left(hir::ModuleDef::Trait(trait_)) => Some(trait_), - _ => None, - }) - .flat_map(|trait_| { - current_module - .find_use_path(ctx.sema.db, hir::ModuleDef::Trait(trait_)) - .as_ref() - .map(mod_path_to_ast) - .zip(Some(trait_)) - }); + let found_traits = + items_locator::with_exact_name(&ctx.sema, current_crate, trait_token.text().to_string()) + .into_iter() + .filter_map(|item| match ModuleDef::from(item.as_module_def_id()?) { + ModuleDef::Trait(trait_) => Some(trait_), + _ => None, + }) + .flat_map(|trait_| { + current_module + .find_use_path(ctx.sema.db, hir::ModuleDef::Trait(trait_)) + .as_ref() + .map(mod_path_to_ast) + .zip(Some(trait_)) + }); let mut no_traits_found = true; for (trait_path, trait_) in found_traits.inspect(|_| no_traits_found = false) { diff --git a/crates/ide_completion/src/completions/flyimport.rs b/crates/ide_completion/src/completions/flyimport.rs index f34764b610..391a11c91c 100644 --- a/crates/ide_completion/src/completions/flyimport.rs +++ b/crates/ide_completion/src/completions/flyimport.rs @@ -21,6 +21,46 @@ //! ``` //! //! Also completes associated items, that require trait imports. +//! If any unresolved and/or partially-qualified path predeces the input, it will be taken into account. +//! Currently, only the imports with their import path ending with the whole qialifier will be proposed +//! (no fuzzy matching for qualifier). +//! +//! ``` +//! mod foo { +//! pub mod bar { +//! pub struct Item; +//! +//! impl Item { +//! pub const TEST_ASSOC: usize = 3; +//! } +//! } +//! } +//! +//! fn main() { +//! bar::Item::TEST_A$0 +//! } +//! ``` +//! -> +//! ``` +//! use foo::bar; +//! +//! mod foo { +//! pub mod bar { +//! pub struct Item; +//! +//! impl Item { +//! pub const TEST_ASSOC: usize = 3; +//! } +//! } +//! } +//! +//! fn main() { +//! bar::Item::TEST_ASSOC +//! } +//! ``` +//! +//! NOTE: currently, if an assoc item comes from a trait that's not currently imported and it also has an unresolved and/or partially-qualified path, +//! no imports will be proposed. //! //! .Fuzzy search details //! @@ -48,12 +88,12 @@ //! Note that having this flag set to `true` does not guarantee that the feature is enabled: your client needs to have the corredponding //! capability enabled. -use hir::{AsAssocItem, ModPath, ScopeDef}; +use hir::ModPath; use ide_db::helpers::{ import_assets::{ImportAssets, ImportCandidate}, insert_use::ImportScope, }; -use rustc_hash::FxHashSet; +use itertools::Itertools; use syntax::{AstNode, SyntaxNode, T}; use crate::{ @@ -92,50 +132,26 @@ pub(crate) fn import_on_the_fly(acc: &mut Completions, ctx: &CompletionContext) &ctx.sema, )?; - let scope_definitions = scope_definitions(ctx); - let mut all_mod_paths = import_assets - .search_for_imports(&ctx.sema, ctx.config.insert_use.prefix_kind) - .into_iter() - .map(|(mod_path, item_in_ns)| { - let scope_item = match item_in_ns { - hir::ItemInNs::Types(id) => ScopeDef::ModuleDef(id.into()), - hir::ItemInNs::Values(id) => ScopeDef::ModuleDef(id.into()), - hir::ItemInNs::Macros(id) => ScopeDef::MacroDef(id.into()), - }; - (mod_path, scope_item) - }) - .filter(|(_, proposed_def)| !scope_definitions.contains(proposed_def)) - .collect::>(); - all_mod_paths.sort_by_cached_key(|(mod_path, _)| { - compute_fuzzy_completion_order_key(mod_path, &user_input_lowercased) - }); - - acc.add_all(all_mod_paths.into_iter().filter_map(|(import_path, definition)| { - let import_for_trait_assoc_item = match definition { - ScopeDef::ModuleDef(module_def) => module_def - .as_assoc_item(ctx.db) - .and_then(|assoc| assoc.containing_trait(ctx.db)) - .is_some(), - _ => false, - }; - let import_edit = ImportEdit { - import_path, - import_scope: import_scope.clone(), - import_for_trait_assoc_item, - }; - render_resolution_with_import(RenderContext::new(ctx), import_edit, &definition) - })); + acc.add_all( + import_assets + .search_for_imports(&ctx.sema, ctx.config.insert_use.prefix_kind) + .into_iter() + .sorted_by_key(|located_import| { + compute_fuzzy_completion_order_key( + &located_import.import_path, + &user_input_lowercased, + ) + }) + .filter_map(|import| { + render_resolution_with_import( + RenderContext::new(ctx), + ImportEdit { import, scope: import_scope.clone() }, + ) + }), + ); Some(()) } -fn scope_definitions(ctx: &CompletionContext) -> FxHashSet { - let mut scope_definitions = FxHashSet::default(); - ctx.scope.process_all_names(&mut |_, scope_def| { - scope_definitions.insert(scope_def); - }); - scope_definitions -} - pub(crate) fn position_for_import<'a>( ctx: &'a CompletionContext, import_candidate: Option<&ImportCandidate>, @@ -160,23 +176,30 @@ fn import_assets(ctx: &CompletionContext, fuzzy_name: String) -> Option s.syntax().clone(), + hir::ModuleSource::Module(m) => m.syntax().clone(), + hir::ModuleSource::BlockExpr(b) => b.syntax().clone(), + }; let assets_for_path = ImportAssets::for_fuzzy_path( current_module, ctx.path_qual.clone(), fuzzy_name, &ctx.sema, - ); + approximate_node, + )?; - if matches!(assets_for_path.as_ref()?.import_candidate(), ImportCandidate::Path(_)) + if matches!(assets_for_path.import_candidate(), ImportCandidate::Path(_)) && fuzzy_name_length < 2 { cov_mark::hit!(ignore_short_input_for_path); None } else { - assets_for_path + Some(assets_for_path) } } } @@ -186,11 +209,11 @@ fn compute_fuzzy_completion_order_key( user_input_lowercased: &str, ) -> usize { cov_mark::hit!(certain_fuzzy_order_test); - let proposed_import_name = match proposed_mod_path.segments().last() { + let import_name = match proposed_mod_path.segments().last() { Some(name) => name.to_string().to_lowercase(), None => return usize::MAX, }; - match proposed_import_name.match_indices(user_input_lowercased).next() { + match import_name.match_indices(user_input_lowercased).next() { Some((first_matching_index, _)) => first_matching_index, None => usize::MAX, } @@ -773,4 +796,155 @@ fn main() { }"#, ); } + + #[test] + fn unresolved_qualifier() { + let fixture = r#" +mod foo { + pub mod bar { + pub mod baz { + pub struct Item; + } + } +} + +fn main() { + bar::baz::Ite$0 +}"#; + + check( + fixture, + expect![[r#" + st foo::bar::baz::Item + "#]], + ); + + check_edit( + "Item", + fixture, + r#" + use foo::bar; + + mod foo { + pub mod bar { + pub mod baz { + pub struct Item; + } + } + } + + fn main() { + bar::baz::Item + }"#, + ); + } + + #[test] + fn unresolved_assoc_item_container() { + let fixture = r#" +mod foo { + pub struct Item; + + impl Item { + pub const TEST_ASSOC: usize = 3; + } +} + +fn main() { + Item::TEST_A$0 +}"#; + + check( + fixture, + expect![[r#" + ct TEST_ASSOC (foo::Item) + "#]], + ); + + check_edit( + "TEST_ASSOC", + fixture, + r#" +use foo::Item; + +mod foo { + pub struct Item; + + impl Item { + pub const TEST_ASSOC: usize = 3; + } +} + +fn main() { + Item::TEST_ASSOC +}"#, + ); + } + + #[test] + fn unresolved_assoc_item_container_with_path() { + let fixture = r#" +mod foo { + pub mod bar { + pub struct Item; + + impl Item { + pub const TEST_ASSOC: usize = 3; + } + } +} + +fn main() { + bar::Item::TEST_A$0 +}"#; + + check( + fixture, + expect![[r#" + ct TEST_ASSOC (foo::bar::Item) + "#]], + ); + + check_edit( + "TEST_ASSOC", + fixture, + r#" +use foo::bar; + +mod foo { + pub mod bar { + pub struct Item; + + impl Item { + pub const TEST_ASSOC: usize = 3; + } + } +} + +fn main() { + bar::Item::TEST_ASSOC +}"#, + ); + } + + #[test] + fn fuzzy_unresolved_path() { + check( + r#" +mod foo { + pub mod bar { + pub struct Item; + + impl Item { + pub const TEST_ASSOC: usize = 3; + } + } +} + +fn main() { + bar::Ass$0 +}"#, + expect![[]], + ) + } } diff --git a/crates/ide_completion/src/item.rs b/crates/ide_completion/src/item.rs index 9b2435c4b6..9b039e3e5f 100644 --- a/crates/ide_completion/src/item.rs +++ b/crates/ide_completion/src/item.rs @@ -2,15 +2,16 @@ use std::fmt; -use hir::{Documentation, ModPath, Mutability}; +use hir::{Documentation, Mutability}; use ide_db::{ helpers::{ + import_assets::LocatedImport, insert_use::{self, ImportScope, InsertUseConfig}, mod_path_to_ast, SnippetCap, }, SymbolKind, }; -use stdx::{impl_from, never}; +use stdx::{format_to, impl_from, never}; use syntax::{algo, TextRange}; use text_edit::TextEdit; @@ -272,9 +273,8 @@ impl CompletionItem { /// An extra import to add after the completion is applied. #[derive(Debug, Clone)] pub struct ImportEdit { - pub import_path: ModPath, - pub import_scope: ImportScope, - pub import_for_trait_assoc_item: bool, + pub import: LocatedImport, + pub scope: ImportScope, } impl ImportEdit { @@ -284,7 +284,7 @@ impl ImportEdit { let _p = profile::span("ImportEdit::to_text_edit"); let rewriter = - insert_use::insert_use(&self.import_scope, mod_path_to_ast(&self.import_path), cfg); + insert_use::insert_use(&self.scope, mod_path_to_ast(&self.import.import_path), cfg); let old_ast = rewriter.rewrite_root()?; let mut import_insert = TextEdit::builder(); algo::diff(&old_ast, &rewriter.rewrite(&old_ast)).into_text_edit(&mut import_insert); @@ -322,20 +322,19 @@ impl Builder { let mut lookup = self.lookup; let mut insert_text = self.insert_text; - if let Some(import_to_add) = self.import_to_add.as_ref() { - if import_to_add.import_for_trait_assoc_item { - lookup = lookup.or_else(|| Some(label.clone())); - insert_text = insert_text.or_else(|| Some(label.clone())); - label = format!("{} ({})", label, import_to_add.import_path); - } else { - let mut import_path_without_last_segment = import_to_add.import_path.to_owned(); - let _ = import_path_without_last_segment.pop_segment(); + if let Some(original_path) = self + .import_to_add + .as_ref() + .and_then(|import_edit| import_edit.import.original_path.as_ref()) + { + lookup = lookup.or_else(|| Some(label.clone())); + insert_text = insert_text.or_else(|| Some(label.clone())); - if !import_path_without_last_segment.segments().is_empty() { - lookup = lookup.or_else(|| Some(label.clone())); - insert_text = insert_text.or_else(|| Some(label.clone())); - label = format!("{}::{}", import_path_without_last_segment, label); - } + let original_path_label = original_path.to_string(); + if original_path_label.ends_with(&label) { + label = original_path_label; + } else { + format_to!(label, " ({})", original_path) } } @@ -439,9 +438,3 @@ impl Builder { self } } - -impl<'a> Into for Builder { - fn into(self) -> CompletionItem { - self.build() - } -} diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs index b0b809791e..a0c8c374d0 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -13,7 +13,9 @@ mod completions; use completions::flyimport::position_for_import; use ide_db::{ - base_db::FilePosition, helpers::insert_use::ImportScope, imports_locator, RootDatabase, + base_db::FilePosition, + helpers::{import_assets::LocatedImport, insert_use::ImportScope}, + items_locator, RootDatabase, }; use text_edit::TextEdit; @@ -139,25 +141,27 @@ pub fn resolve_completion_edits( position: FilePosition, full_import_path: &str, imported_name: String, - import_for_trait_assoc_item: bool, ) -> Option> { let ctx = CompletionContext::new(db, position, config)?; let position_for_import = position_for_import(&ctx, None)?; - let import_scope = ImportScope::find_insert_use_container(position_for_import, &ctx.sema)?; + let scope = ImportScope::find_insert_use_container(position_for_import, &ctx.sema)?; let current_module = ctx.sema.scope(position_for_import).module()?; let current_crate = current_module.krate(); - let import_path = imports_locator::find_exact_imports(&ctx.sema, current_crate, imported_name) - .filter_map(|candidate| { - let item: hir::ItemInNs = candidate.either(Into::into, Into::into); - current_module.find_use_path_prefixed(db, item, config.insert_use.prefix_kind) - }) - .find(|mod_path| mod_path.to_string() == full_import_path)?; + let (import_path, item_to_import) = + items_locator::with_exact_name(&ctx.sema, current_crate, imported_name) + .into_iter() + .filter_map(|candidate| { + current_module + .find_use_path_prefixed(db, candidate, config.insert_use.prefix_kind) + .zip(Some(candidate)) + }) + .find(|(mod_path, _)| mod_path.to_string() == full_import_path)?; + let import = + LocatedImport::new(import_path.clone(), item_to_import, item_to_import, Some(import_path)); - ImportEdit { import_path, import_scope, import_for_trait_assoc_item } - .to_text_edit(config.insert_use) - .map(|edit| vec![edit]) + ImportEdit { import, scope }.to_text_edit(config.insert_use).map(|edit| vec![edit]) } #[cfg(test)] diff --git a/crates/ide_completion/src/render.rs b/crates/ide_completion/src/render.rs index dcfac23c55..fae5685e22 100644 --- a/crates/ide_completion/src/render.rs +++ b/crates/ide_completion/src/render.rs @@ -13,7 +13,10 @@ mod builder_ext; use hir::{ AsAssocItem, Documentation, HasAttrs, HirDisplay, ModuleDef, Mutability, ScopeDef, Type, }; -use ide_db::{helpers::SnippetCap, RootDatabase, SymbolKind}; +use ide_db::{ + helpers::{item_name, SnippetCap}, + RootDatabase, SymbolKind, +}; use syntax::TextRange; use crate::{ @@ -50,18 +53,20 @@ pub(crate) fn render_resolution<'a>( pub(crate) fn render_resolution_with_import<'a>( ctx: RenderContext<'a>, import_edit: ImportEdit, - resolution: &ScopeDef, ) -> Option { + let resolution = ScopeDef::from(import_edit.import.original_item); let local_name = match resolution { ScopeDef::ModuleDef(ModuleDef::Function(f)) => f.name(ctx.completion.db).to_string(), ScopeDef::ModuleDef(ModuleDef::Const(c)) => c.name(ctx.completion.db)?.to_string(), ScopeDef::ModuleDef(ModuleDef::TypeAlias(t)) => t.name(ctx.completion.db).to_string(), - _ => import_edit.import_path.segments().last()?.to_string(), + _ => item_name(ctx.db(), import_edit.import.original_item)?.to_string(), }; - Render::new(ctx).render_resolution(local_name, Some(import_edit), resolution).map(|mut item| { - item.completion_kind = CompletionKind::Magic; - item - }) + Render::new(ctx).render_resolution(local_name, Some(import_edit), &resolution).map( + |mut item| { + item.completion_kind = CompletionKind::Magic; + item + }, + ) } /// Interface for data and methods required for items rendering. diff --git a/crates/ide_db/src/helpers.rs b/crates/ide_db/src/helpers.rs index 3ff77400bb..3c95d3cffa 100644 --- a/crates/ide_db/src/helpers.rs +++ b/crates/ide_db/src/helpers.rs @@ -2,11 +2,19 @@ pub mod insert_use; pub mod import_assets; -use hir::{Crate, Enum, Module, ScopeDef, Semantics, Trait}; +use hir::{Crate, Enum, ItemInNs, MacroDef, Module, ModuleDef, Name, ScopeDef, Semantics, Trait}; use syntax::ast::{self, make}; use crate::RootDatabase; +pub fn item_name(db: &RootDatabase, item: ItemInNs) -> Option { + match item { + ItemInNs::Types(module_def_id) => ModuleDef::from(module_def_id).name(db), + ItemInNs::Values(module_def_id) => ModuleDef::from(module_def_id).name(db), + ItemInNs::Macros(macro_def_id) => MacroDef::from(macro_def_id).name(db), + } +} + /// Converts the mod path struct into its ast representation. pub fn mod_path_to_ast(path: &hir::ModPath) -> ast::Path { let _p = profile::span("mod_path_to_ast"); diff --git a/crates/ide_db/src/helpers/import_assets.rs b/crates/ide_db/src/helpers/import_assets.rs index 517abbb4bc..e03ccd3515 100644 --- a/crates/ide_db/src/helpers/import_assets.rs +++ b/crates/ide_db/src/helpers/import_assets.rs @@ -1,19 +1,29 @@ //! Look up accessible paths for items. -use either::Either; -use hir::{AsAssocItem, AssocItem, Crate, MacroDef, Module, ModuleDef, PrefixKind, Semantics}; +use hir::{ + AsAssocItem, AssocItem, AssocItemContainer, Crate, ItemInNs, MacroDef, ModPath, Module, + ModuleDef, PathResolution, PrefixKind, ScopeDef, Semantics, Type, +}; +use itertools::Itertools; use rustc_hash::FxHashSet; -use syntax::{ast, AstNode}; +use syntax::{ast, utils::path_to_string_stripping_turbo_fish, AstNode, SyntaxNode}; use crate::{ - imports_locator::{self, AssocItemSearch, DEFAULT_QUERY_SEARCH_LIMIT}, + items_locator::{self, AssocItemSearch, DEFAULT_QUERY_SEARCH_LIMIT}, RootDatabase, }; +use super::item_name; + +/// A candidate for import, derived during various IDE activities: +/// * completion with imports on the fly proposals +/// * completion edit resolve requests +/// * assists +/// * etc. #[derive(Debug)] pub enum ImportCandidate { - // A path, qualified (`std::collections::HashMap`) or not (`HashMap`). + /// A path, qualified (`std::collections::HashMap`) or not (`HashMap`). Path(PathImportCandidate), - /// A trait associated function (with no self parameter) or associated constant. + /// A trait associated function (with no self parameter) or an associated constant. /// For 'test_mod::TestEnum::test_function', `ty` is the `test_mod::TestEnum` expression type /// and `name` is the `test_function` TraitAssocItem(TraitImportCandidate), @@ -23,21 +33,40 @@ pub enum ImportCandidate { TraitMethod(TraitImportCandidate), } +/// A trait import needed for a given associated item access. +/// For `some::path::SomeStruct::ASSOC_`, contains the +/// type of `some::path::SomeStruct` and `ASSOC_` as the item name. #[derive(Debug)] pub struct TraitImportCandidate { - pub receiver_ty: hir::Type, - pub name: NameToImport, + /// A type of the item that has the associated item accessed at. + pub receiver_ty: Type, + /// The associated item name that the trait to import should contain. + pub assoc_item_name: NameToImport, } +/// Path import for a given name, qualified or not. #[derive(Debug)] pub struct PathImportCandidate { - pub qualifier: Option, + /// Optional qualifier before name. + pub qualifier: Option, + /// The name the item (struct, trait, enum, etc.) should have. pub name: NameToImport, } +/// A qualifier that has a first segment and it's unresolved. +#[derive(Debug)] +pub struct FirstSegmentUnresolved { + fist_segment: ast::NameRef, + full_qualifier: ast::Path, +} + +/// A name that will be used during item lookups. #[derive(Debug)] pub enum NameToImport { + /// Requires items with names that exactly match the given string, case-sensitive. Exact(String), + /// Requires items with names that case-insensitively contain all letters from the string, + /// in the same order, but not necessary adjacent. Fuzzy(String), } @@ -50,10 +79,12 @@ impl NameToImport { } } +/// A struct to find imports in the project, given a certain name (or its part) and the context. #[derive(Debug)] pub struct ImportAssets { import_candidate: ImportCandidate, - module_with_candidate: hir::Module, + candidate_node: SyntaxNode, + module_with_candidate: Module, } impl ImportAssets { @@ -61,9 +92,11 @@ impl ImportAssets { method_call: &ast::MethodCallExpr, sema: &Semantics, ) -> Option { + let candidate_node = method_call.syntax().clone(); Some(Self { import_candidate: ImportCandidate::for_method_call(sema, method_call)?, - module_with_candidate: sema.scope(method_call.syntax()).module()?, + module_with_candidate: sema.scope(&candidate_node).module()?, + candidate_node, }) } @@ -71,94 +104,94 @@ impl ImportAssets { fully_qualified_path: &ast::Path, sema: &Semantics, ) -> Option { - let syntax_under_caret = fully_qualified_path.syntax(); - if syntax_under_caret.ancestors().find_map(ast::Use::cast).is_some() { + let candidate_node = fully_qualified_path.syntax().clone(); + if candidate_node.ancestors().find_map(ast::Use::cast).is_some() { return None; } Some(Self { import_candidate: ImportCandidate::for_regular_path(sema, fully_qualified_path)?, - module_with_candidate: sema.scope(syntax_under_caret).module()?, + module_with_candidate: sema.scope(&candidate_node).module()?, + candidate_node, }) } pub fn for_fuzzy_path( - module_with_path: Module, + module_with_candidate: Module, qualifier: Option, fuzzy_name: String, sema: &Semantics, + candidate_node: SyntaxNode, ) -> Option { - Some(match qualifier { - Some(qualifier) => { - let qualifier_resolution = sema.resolve_path(&qualifier)?; - match qualifier_resolution { - hir::PathResolution::Def(hir::ModuleDef::Adt(assoc_item_path)) => Self { - import_candidate: ImportCandidate::TraitAssocItem(TraitImportCandidate { - receiver_ty: assoc_item_path.ty(sema.db), - name: NameToImport::Fuzzy(fuzzy_name), - }), - module_with_candidate: module_with_path, - }, - _ => Self { - import_candidate: ImportCandidate::Path(PathImportCandidate { - qualifier: Some(qualifier), - name: NameToImport::Fuzzy(fuzzy_name), - }), - module_with_candidate: module_with_path, - }, - } - } - None => Self { - import_candidate: ImportCandidate::Path(PathImportCandidate { - qualifier: None, - name: NameToImport::Fuzzy(fuzzy_name), - }), - module_with_candidate: module_with_path, - }, + Some(Self { + import_candidate: ImportCandidate::for_fuzzy_path(qualifier, fuzzy_name, sema)?, + module_with_candidate, + candidate_node, }) } pub fn for_fuzzy_method_call( module_with_method_call: Module, - receiver_ty: hir::Type, + receiver_ty: Type, fuzzy_method_name: String, + candidate_node: SyntaxNode, ) -> Option { Some(Self { import_candidate: ImportCandidate::TraitMethod(TraitImportCandidate { receiver_ty, - name: NameToImport::Fuzzy(fuzzy_method_name), + assoc_item_name: NameToImport::Fuzzy(fuzzy_method_name), }), module_with_candidate: module_with_method_call, + candidate_node, }) } } +/// An import (not necessary the only one) that corresponds a certain given [`PathImportCandidate`]. +/// (the structure is not entirely correct, since there can be situations requiring two imports, see FIXME below for the details) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LocatedImport { + /// The path to use in the `use` statement for a given candidate to be imported. + pub import_path: ModPath, + /// An item that will be imported with the import path given. + pub item_to_import: ItemInNs, + /// The path import candidate, resolved. + /// + /// Not necessary matches the import: + /// For any associated constant from the trait, we try to access as `some::path::SomeStruct::ASSOC_` + /// the original item is the associated constant, but the import has to be a trait that + /// defines this constant. + pub original_item: ItemInNs, + /// A path of the original item. + pub original_path: Option, +} + +impl LocatedImport { + pub fn new( + import_path: ModPath, + item_to_import: ItemInNs, + original_item: ItemInNs, + original_path: Option, + ) -> Self { + Self { import_path, item_to_import, original_item, original_path } + } +} + impl ImportAssets { pub fn import_candidate(&self) -> &ImportCandidate { &self.import_candidate } - fn name_to_import(&self) -> &NameToImport { - match &self.import_candidate { - ImportCandidate::Path(candidate) => &candidate.name, - ImportCandidate::TraitAssocItem(candidate) - | ImportCandidate::TraitMethod(candidate) => &candidate.name, - } - } - pub fn search_for_imports( &self, sema: &Semantics, prefix_kind: PrefixKind, - ) -> Vec<(hir::ModPath, hir::ItemInNs)> { + ) -> Vec { let _p = profile::span("import_assets::search_for_imports"); self.search_for(sema, Some(prefix_kind)) } /// This may return non-absolute paths if a part of the returned path is already imported into scope. - pub fn search_for_relative_paths( - &self, - sema: &Semantics, - ) -> Vec<(hir::ModPath, hir::ItemInNs)> { + pub fn search_for_relative_paths(&self, sema: &Semantics) -> Vec { let _p = profile::span("import_assets::search_for_relative_paths"); self.search_for(sema, None) } @@ -166,29 +199,29 @@ impl ImportAssets { fn search_for( &self, sema: &Semantics, - prefixed: Option, - ) -> Vec<(hir::ModPath, hir::ItemInNs)> { - let current_crate = self.module_with_candidate.krate(); - - let unfiltered_imports = match self.name_to_import() { - NameToImport::Exact(exact_name) => { - imports_locator::find_exact_imports(sema, current_crate, exact_name.clone()) - } + prefixed: Option, + ) -> Vec { + let items_with_candidate_name = match self.name_to_import() { + NameToImport::Exact(exact_name) => items_locator::with_exact_name( + sema, + self.module_with_candidate.krate(), + exact_name.clone(), + ), // FIXME: ideally, we should avoid using `fst` for seacrhing trait imports for assoc items: // instead, we need to look up all trait impls for a certain struct and search through them only // see https://github.com/rust-analyzer/rust-analyzer/pull/7293#issuecomment-761585032 // and https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0/topic/Blanket.20trait.20impls.20lookup // for the details NameToImport::Fuzzy(fuzzy_name) => { - let (assoc_item_search, limit) = match self.import_candidate { - ImportCandidate::TraitAssocItem(_) | ImportCandidate::TraitMethod(_) => { - (AssocItemSearch::AssocItemsOnly, None) - } - _ => (AssocItemSearch::Exclude, Some(DEFAULT_QUERY_SEARCH_LIMIT)), + let (assoc_item_search, limit) = if self.import_candidate.is_trait_candidate() { + (AssocItemSearch::AssocItemsOnly, None) + } else { + (AssocItemSearch::Include, Some(DEFAULT_QUERY_SEARCH_LIMIT)) }; - imports_locator::find_similar_imports( + + items_locator::with_similar_name( sema, - current_crate, + self.module_with_candidate.krate(), fuzzy_name.clone(), assoc_item_search, limit, @@ -196,63 +229,224 @@ impl ImportAssets { } }; - let db = sema.db; - let mut res = - applicable_defs(self.import_candidate(), current_crate, db, unfiltered_imports) - .filter_map(|candidate| { - let item: hir::ItemInNs = candidate.clone().either(Into::into, Into::into); + let scope_definitions = self.scope_definitions(sema); + self.applicable_defs(sema.db, prefixed, items_with_candidate_name) + .into_iter() + .filter(|import| import.import_path.len() > 1) + .filter(|import| !scope_definitions.contains(&ScopeDef::from(import.item_to_import))) + .sorted_by_key(|import| import.import_path.clone()) + .collect() + } - let item_to_search = match self.import_candidate { - ImportCandidate::TraitAssocItem(_) | ImportCandidate::TraitMethod(_) => { - let canidate_trait = match candidate { - Either::Left(module_def) => { - module_def.as_assoc_item(db)?.containing_trait(db) - } - _ => None, - }?; - ModuleDef::from(canidate_trait).into() - } - _ => item, - }; + fn scope_definitions(&self, sema: &Semantics) -> FxHashSet { + let mut scope_definitions = FxHashSet::default(); + sema.scope(&self.candidate_node).process_all_names(&mut |_, scope_def| { + scope_definitions.insert(scope_def); + }); + scope_definitions + } - if let Some(prefix_kind) = prefixed { - self.module_with_candidate.find_use_path_prefixed( - db, - item_to_search, - prefix_kind, - ) - } else { - self.module_with_candidate.find_use_path(db, item_to_search) - } - .map(|path| (path, item)) - }) - .filter(|(use_path, _)| use_path.len() > 1) - .collect::>(); - res.sort_by_cached_key(|(path, _)| path.clone()); - res + fn name_to_import(&self) -> &NameToImport { + match &self.import_candidate { + ImportCandidate::Path(candidate) => &candidate.name, + ImportCandidate::TraitAssocItem(candidate) + | ImportCandidate::TraitMethod(candidate) => &candidate.assoc_item_name, + } + } + + fn applicable_defs( + &self, + db: &RootDatabase, + prefixed: Option, + items_with_candidate_name: FxHashSet, + ) -> FxHashSet { + let _p = profile::span("import_assets::applicable_defs"); + let current_crate = self.module_with_candidate.krate(); + + let mod_path = |item| { + get_mod_path(db, item_for_path_search(db, item)?, &self.module_with_candidate, prefixed) + }; + + match &self.import_candidate { + ImportCandidate::Path(path_candidate) => { + path_applicable_imports(db, path_candidate, mod_path, items_with_candidate_name) + } + ImportCandidate::TraitAssocItem(trait_candidate) => trait_applicable_items( + db, + current_crate, + trait_candidate, + true, + mod_path, + items_with_candidate_name, + ), + ImportCandidate::TraitMethod(trait_candidate) => trait_applicable_items( + db, + current_crate, + trait_candidate, + false, + mod_path, + items_with_candidate_name, + ), + } } } -fn applicable_defs<'a>( - import_candidate: &ImportCandidate, - current_crate: Crate, +fn path_applicable_imports( db: &RootDatabase, - unfiltered_imports: Box> + 'a>, -) -> Box> + 'a> { - let receiver_ty = match import_candidate { - ImportCandidate::Path(_) => return unfiltered_imports, - ImportCandidate::TraitAssocItem(candidate) | ImportCandidate::TraitMethod(candidate) => { - &candidate.receiver_ty + path_candidate: &PathImportCandidate, + mod_path: impl Fn(ItemInNs) -> Option + Copy, + items_with_candidate_name: FxHashSet, +) -> FxHashSet { + let _p = profile::span("import_assets::path_applicable_imports"); + + let (unresolved_first_segment, unresolved_qualifier) = match &path_candidate.qualifier { + None => { + return items_with_candidate_name + .into_iter() + .filter_map(|item| { + Some(LocatedImport::new(mod_path(item)?, item, item, mod_path(item))) + }) + .collect(); } + Some(first_segment_unresolved) => ( + first_segment_unresolved.fist_segment.to_string(), + path_to_string_stripping_turbo_fish(&first_segment_unresolved.full_qualifier), + ), }; + items_with_candidate_name + .into_iter() + .filter_map(|item| { + import_for_item(db, mod_path, &unresolved_first_segment, &unresolved_qualifier, item) + }) + .collect() +} + +fn import_for_item( + db: &RootDatabase, + mod_path: impl Fn(ItemInNs) -> Option, + unresolved_first_segment: &str, + unresolved_qualifier: &str, + original_item: ItemInNs, +) -> Option { + let _p = profile::span("import_assets::import_for_item"); + + let original_item_candidate = item_for_path_search(db, original_item)?; + let import_path_candidate = mod_path(original_item_candidate)?; + let import_path_string = import_path_candidate.to_string(); + + let expected_import_end = if item_as_assoc(db, original_item).is_some() { + unresolved_qualifier.to_string() + } else { + format!("{}::{}", unresolved_qualifier, item_name(db, original_item)?) + }; + if !import_path_string.contains(unresolved_first_segment) + || !import_path_string.ends_with(&expected_import_end) + { + return None; + } + + let segment_import = + find_import_for_segment(db, original_item_candidate, &unresolved_first_segment)?; + let trait_item_to_import = item_as_assoc(db, original_item) + .and_then(|assoc| assoc.containing_trait(db)) + .map(|trait_| ItemInNs::from(ModuleDef::from(trait_))); + Some(match (segment_import == original_item_candidate, trait_item_to_import) { + (true, Some(_)) => { + // FIXME we should be able to import both the trait and the segment, + // but it's unclear what to do with overlapping edits (merge imports?) + // especially in case of lazy completion edit resolutions. + return None; + } + (false, Some(trait_to_import)) => LocatedImport::new( + mod_path(trait_to_import)?, + trait_to_import, + original_item, + mod_path(original_item), + ), + (true, None) => LocatedImport::new( + import_path_candidate, + original_item_candidate, + original_item, + mod_path(original_item), + ), + (false, None) => LocatedImport::new( + mod_path(segment_import)?, + segment_import, + original_item, + mod_path(original_item), + ), + }) +} + +fn item_for_path_search(db: &RootDatabase, item: ItemInNs) -> Option { + Some(match item { + ItemInNs::Types(_) | ItemInNs::Values(_) => match item_as_assoc(db, item) { + Some(assoc_item) => match assoc_item.container(db) { + AssocItemContainer::Trait(trait_) => ItemInNs::from(ModuleDef::from(trait_)), + AssocItemContainer::Impl(impl_) => { + ItemInNs::from(ModuleDef::from(impl_.target_ty(db).as_adt()?)) + } + }, + None => item, + }, + ItemInNs::Macros(_) => item, + }) +} + +fn find_import_for_segment( + db: &RootDatabase, + original_item: ItemInNs, + unresolved_first_segment: &str, +) -> Option { + let segment_is_name = item_name(db, original_item) + .map(|name| name.to_string() == unresolved_first_segment) + .unwrap_or(false); + + Some(if segment_is_name { + original_item + } else { + let matching_module = + module_with_segment_name(db, &unresolved_first_segment, original_item)?; + ItemInNs::from(ModuleDef::from(matching_module)) + }) +} + +fn module_with_segment_name( + db: &RootDatabase, + segment_name: &str, + candidate: ItemInNs, +) -> Option { + let mut current_module = match candidate { + ItemInNs::Types(module_def_id) => ModuleDef::from(module_def_id).module(db), + ItemInNs::Values(module_def_id) => ModuleDef::from(module_def_id).module(db), + ItemInNs::Macros(macro_def_id) => MacroDef::from(macro_def_id).module(db), + }; + while let Some(module) = current_module { + if let Some(module_name) = module.name(db) { + if module_name.to_string() == segment_name { + return Some(module); + } + } + current_module = module.parent(db); + } + None +} + +fn trait_applicable_items( + db: &RootDatabase, + current_crate: Crate, + trait_candidate: &TraitImportCandidate, + trait_assoc_item: bool, + mod_path: impl Fn(ItemInNs) -> Option, + items_with_candidate_name: FxHashSet, +) -> FxHashSet { + let _p = profile::span("import_assets::trait_applicable_items"); let mut required_assoc_items = FxHashSet::default(); - let trait_candidates = unfiltered_imports - .filter_map(|input| match input { - Either::Left(module_def) => module_def.as_assoc_item(db), - _ => None, - }) + let trait_candidates = items_with_candidate_name + .into_iter() + .filter_map(|input| item_as_assoc(db, input)) .filter_map(|assoc| { let assoc_item_trait = assoc.containing_trait(db)?; required_assoc_items.insert(assoc); @@ -260,11 +454,10 @@ fn applicable_defs<'a>( }) .collect(); - let mut applicable_defs = FxHashSet::default(); + let mut located_imports = FxHashSet::default(); - match import_candidate { - ImportCandidate::Path(_) => unreachable!(), - ImportCandidate::TraitAssocItem(_) => receiver_ty.iterate_path_candidates( + if trait_assoc_item { + trait_candidate.receiver_ty.iterate_path_candidates( db, current_crate, &trait_candidates, @@ -276,12 +469,21 @@ fn applicable_defs<'a>( return None; } } - applicable_defs.insert(Either::Left(assoc_to_module_def(assoc))); + + let item = ItemInNs::from(ModuleDef::from(assoc.containing_trait(db)?)); + let original_item = assoc_to_item(assoc); + located_imports.insert(LocatedImport::new( + mod_path(item)?, + item, + original_item, + mod_path(original_item), + )); } None::<()> }, - ), - ImportCandidate::TraitMethod(_) => receiver_ty.iterate_method_candidates( + ) + } else { + trait_candidate.receiver_ty.iterate_method_candidates( db, current_crate, &trait_candidates, @@ -289,21 +491,41 @@ fn applicable_defs<'a>( |_, function| { let assoc = function.as_assoc_item(db)?; if required_assoc_items.contains(&assoc) { - applicable_defs.insert(Either::Left(assoc_to_module_def(assoc))); + let item = ItemInNs::from(ModuleDef::from(assoc.containing_trait(db)?)); + let original_item = assoc_to_item(assoc); + located_imports.insert(LocatedImport::new( + mod_path(item)?, + item, + original_item, + mod_path(original_item), + )); } None::<()> }, - ), + ) }; - Box::new(applicable_defs.into_iter()) + located_imports } -fn assoc_to_module_def(assoc: AssocItem) -> ModuleDef { +fn assoc_to_item(assoc: AssocItem) -> ItemInNs { match assoc { - AssocItem::Function(f) => f.into(), - AssocItem::Const(c) => c.into(), - AssocItem::TypeAlias(t) => t.into(), + AssocItem::Function(f) => ItemInNs::from(ModuleDef::from(f)), + AssocItem::Const(c) => ItemInNs::from(ModuleDef::from(c)), + AssocItem::TypeAlias(t) => ItemInNs::from(ModuleDef::from(t)), + } +} + +fn get_mod_path( + db: &RootDatabase, + item_to_search: ItemInNs, + module_with_candidate: &Module, + prefixed: Option, +) -> Option { + if let Some(prefix_kind) = prefixed { + module_with_candidate.find_use_path_prefixed(db, item_to_search, prefix_kind) + } else { + module_with_candidate.find_use_path(db, item_to_search) } } @@ -316,7 +538,7 @@ impl ImportCandidate { Some(_) => None, None => Some(Self::TraitMethod(TraitImportCandidate { receiver_ty: sema.type_of_expr(&method_call.receiver()?)?, - name: NameToImport::Exact(method_call.name_ref()?.to_string()), + assoc_item_name: NameToImport::Exact(method_call.name_ref()?.to_string()), })), } } @@ -325,41 +547,63 @@ impl ImportCandidate { if sema.resolve_path(path).is_some() { return None; } + path_import_candidate( + sema, + path.qualifier(), + NameToImport::Exact(path.segment()?.name_ref()?.to_string()), + ) + } - let segment = path.segment()?; - let candidate = if let Some(qualifier) = path.qualifier() { - let qualifier_start = qualifier.syntax().descendants().find_map(ast::NameRef::cast)?; - let qualifier_start_path = - qualifier_start.syntax().ancestors().find_map(ast::Path::cast)?; - if let Some(qualifier_start_resolution) = sema.resolve_path(&qualifier_start_path) { - let qualifier_resolution = if qualifier_start_path == qualifier { - qualifier_start_resolution - } else { - sema.resolve_path(&qualifier)? - }; - match qualifier_resolution { - hir::PathResolution::Def(hir::ModuleDef::Adt(assoc_item_path)) => { - ImportCandidate::TraitAssocItem(TraitImportCandidate { - receiver_ty: assoc_item_path.ty(sema.db), - name: NameToImport::Exact(segment.name_ref()?.to_string()), - }) - } - _ => return None, - } - } else { - ImportCandidate::Path(PathImportCandidate { - qualifier: Some(qualifier), - name: NameToImport::Exact(qualifier_start.to_string()), - }) - } - } else { - ImportCandidate::Path(PathImportCandidate { - qualifier: None, - name: NameToImport::Exact( - segment.syntax().descendants().find_map(ast::NameRef::cast)?.to_string(), - ), - }) - }; - Some(candidate) + fn for_fuzzy_path( + qualifier: Option, + fuzzy_name: String, + sema: &Semantics, + ) -> Option { + path_import_candidate(sema, qualifier, NameToImport::Fuzzy(fuzzy_name)) + } + + fn is_trait_candidate(&self) -> bool { + matches!(self, ImportCandidate::TraitAssocItem(_) | ImportCandidate::TraitMethod(_)) } } + +fn path_import_candidate( + sema: &Semantics, + qualifier: Option, + name: NameToImport, +) -> Option { + Some(match qualifier { + Some(qualifier) => match sema.resolve_path(&qualifier) { + None => { + let qualifier_start = + qualifier.syntax().descendants().find_map(ast::NameRef::cast)?; + let qualifier_start_path = + qualifier_start.syntax().ancestors().find_map(ast::Path::cast)?; + if sema.resolve_path(&qualifier_start_path).is_none() { + ImportCandidate::Path(PathImportCandidate { + qualifier: Some(FirstSegmentUnresolved { + fist_segment: qualifier_start, + full_qualifier: qualifier, + }), + name, + }) + } else { + return None; + } + } + Some(PathResolution::Def(ModuleDef::Adt(assoc_item_path))) => { + ImportCandidate::TraitAssocItem(TraitImportCandidate { + receiver_ty: assoc_item_path.ty(sema.db), + assoc_item_name: name, + }) + } + Some(_) => return None, + }, + None => ImportCandidate::Path(PathImportCandidate { qualifier: None, name }), + }) +} + +fn item_as_assoc(db: &RootDatabase, item: ItemInNs) -> Option { + item.as_module_def_id() + .and_then(|module_def_id| ModuleDef::from(module_def_id).as_assoc_item(db)) +} diff --git a/crates/ide_db/src/imports_locator.rs b/crates/ide_db/src/items_locator.rs similarity index 61% rename from crates/ide_db/src/imports_locator.rs rename to crates/ide_db/src/items_locator.rs index 502e8281a3..8a7f029353 100644 --- a/crates/ide_db/src/imports_locator.rs +++ b/crates/ide_db/src/items_locator.rs @@ -1,9 +1,10 @@ //! This module contains an import search functionality that is provided to the assists module. //! Later, this should be moved away to a separate crate that is accessible from the assists module. +use either::Either; use hir::{ import_map::{self, ImportKind}, - AsAssocItem, Crate, MacroDef, ModuleDef, Semantics, + AsAssocItem, Crate, ItemInNs, ModuleDef, Semantics, }; use syntax::{ast, AstNode, SyntaxKind::NAME}; @@ -12,47 +13,47 @@ use crate::{ symbol_index::{self, FileSymbol}, RootDatabase, }; -use either::Either; use rustc_hash::FxHashSet; pub(crate) const DEFAULT_QUERY_SEARCH_LIMIT: usize = 40; -pub fn find_exact_imports<'a>( - sema: &Semantics<'a, RootDatabase>, +pub fn with_exact_name( + sema: &Semantics<'_, RootDatabase>, krate: Crate, - name_to_import: String, -) -> Box>> { + exact_name: String, +) -> FxHashSet { let _p = profile::span("find_exact_imports"); - Box::new(find_imports( + find_items( sema, krate, { - let mut local_query = symbol_index::Query::new(name_to_import.clone()); + let mut local_query = symbol_index::Query::new(exact_name.clone()); local_query.exact(); local_query.limit(DEFAULT_QUERY_SEARCH_LIMIT); local_query }, - import_map::Query::new(name_to_import) + import_map::Query::new(exact_name) .limit(DEFAULT_QUERY_SEARCH_LIMIT) .name_only() .search_mode(import_map::SearchMode::Equals) .case_sensitive(), - )) + ) } +#[derive(Debug)] pub enum AssocItemSearch { Include, Exclude, AssocItemsOnly, } -pub fn find_similar_imports<'a>( - sema: &Semantics<'a, RootDatabase>, +pub fn with_similar_name( + sema: &Semantics<'_, RootDatabase>, krate: Crate, fuzzy_search_string: String, assoc_item_search: AssocItemSearch, limit: Option, -) -> Box> + 'a> { +) -> FxHashSet { let _p = profile::span("find_similar_imports"); let mut external_query = import_map::Query::new(fuzzy_search_string.clone()) @@ -76,37 +77,39 @@ pub fn find_similar_imports<'a>( local_query.limit(limit); } - let db = sema.db; - Box::new(find_imports(sema, krate, local_query, external_query).filter( - move |import_candidate| match assoc_item_search { + find_items(sema, krate, local_query, external_query) + .into_iter() + .filter(move |&item| match assoc_item_search { AssocItemSearch::Include => true, - AssocItemSearch::Exclude => !is_assoc_item(import_candidate, db), - AssocItemSearch::AssocItemsOnly => is_assoc_item(import_candidate, db), - }, - )) + AssocItemSearch::Exclude => !is_assoc_item(item, sema.db), + AssocItemSearch::AssocItemsOnly => is_assoc_item(item, sema.db), + }) + .collect() } -fn is_assoc_item(import_candidate: &Either, db: &RootDatabase) -> bool { - match import_candidate { - Either::Left(ModuleDef::Function(function)) => function.as_assoc_item(db).is_some(), - Either::Left(ModuleDef::Const(const_)) => const_.as_assoc_item(db).is_some(), - Either::Left(ModuleDef::TypeAlias(type_alias)) => type_alias.as_assoc_item(db).is_some(), - _ => false, - } +fn is_assoc_item(item: ItemInNs, db: &RootDatabase) -> bool { + item.as_module_def_id() + .and_then(|module_def_id| ModuleDef::from(module_def_id).as_assoc_item(db)) + .is_some() } -fn find_imports<'a>( - sema: &Semantics<'a, RootDatabase>, +fn find_items( + sema: &Semantics<'_, RootDatabase>, krate: Crate, local_query: symbol_index::Query, external_query: import_map::Query, -) -> impl Iterator> { +) -> FxHashSet { let _p = profile::span("find_similar_imports"); let db = sema.db; // Query dependencies first. - let mut candidates: FxHashSet<_> = - krate.query_external_importables(db, external_query).collect(); + let mut candidates = krate + .query_external_importables(db, external_query) + .map(|external_importable| match external_importable { + Either::Left(module_def) => ItemInNs::from(module_def), + Either::Right(macro_def) => ItemInNs::from(macro_def), + }) + .collect::>(); // Query the local crate using the symbol index. let local_results = symbol_index::crate_symbols(db, krate.into(), local_query); @@ -114,19 +117,19 @@ fn find_imports<'a>( candidates.extend( local_results .into_iter() - .filter_map(|import_candidate| get_name_definition(sema, &import_candidate)) + .filter_map(|local_candidate| get_name_definition(sema, &local_candidate)) .filter_map(|name_definition_to_import| match name_definition_to_import { - Definition::ModuleDef(module_def) => Some(Either::Left(module_def)), - Definition::Macro(macro_def) => Some(Either::Right(macro_def)), + Definition::ModuleDef(module_def) => Some(ItemInNs::from(module_def)), + Definition::Macro(macro_def) => Some(ItemInNs::from(macro_def)), _ => None, }), ); - candidates.into_iter() + candidates } -fn get_name_definition<'a>( - sema: &Semantics<'a, RootDatabase>, +fn get_name_definition( + sema: &Semantics<'_, RootDatabase>, import_candidate: &FileSymbol, ) -> Option { let _p = profile::span("get_name_definition"); diff --git a/crates/ide_db/src/lib.rs b/crates/ide_db/src/lib.rs index 6eb34b06b7..88ee4a87d5 100644 --- a/crates/ide_db/src/lib.rs +++ b/crates/ide_db/src/lib.rs @@ -8,7 +8,7 @@ pub mod line_index; pub mod symbol_index; pub mod defs; pub mod search; -pub mod imports_locator; +pub mod items_locator; pub mod source_change; pub mod ty_filter; pub mod traits; diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index 4f6f250d6c..2c4c339cb7 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -697,7 +697,6 @@ pub(crate) fn handle_completion_resolve( FilePosition { file_id, offset }, &resolve_data.full_import_path, resolve_data.imported_name, - resolve_data.import_for_trait_assoc_item, )? .into_iter() .flat_map(|edit| edit.into_iter().map(|indel| to_proto::text_edit(&line_index, indel))) @@ -1525,7 +1524,6 @@ struct CompletionResolveData { position: lsp_types::TextDocumentPositionParams, full_import_path: String, imported_name: String, - import_for_trait_assoc_item: bool, } fn fill_resolve_data( @@ -1534,15 +1532,13 @@ fn fill_resolve_data( position: &TextDocumentPositionParams, ) -> Option<()> { let import_edit = item.import_to_add()?; - let full_import_path = import_edit.import_path.to_string(); - let imported_name = import_edit.import_path.segments().last()?.to_string(); + let import_path = &import_edit.import.import_path; *resolve_data = Some( to_value(CompletionResolveData { position: position.to_owned(), - full_import_path, - imported_name, - import_for_trait_assoc_item: import_edit.import_for_trait_assoc_item, + full_import_path: import_path.to_string(), + imported_name: import_path.segments().last()?.to_string(), }) .unwrap(), ); diff --git a/crates/syntax/Cargo.toml b/crates/syntax/Cargo.toml index c39095def0..33bde099bc 100644 --- a/crates/syntax/Cargo.toml +++ b/crates/syntax/Cargo.toml @@ -25,10 +25,10 @@ serde = { version = "1.0.106", features = ["derive"] } stdx = { path = "../stdx", version = "0.0.0" } text_edit = { path = "../text_edit", version = "0.0.0" } parser = { path = "../parser", version = "0.0.0" } -test_utils = { path = "../test_utils", version = "0.0.0" } profile = { path = "../profile", version = "0.0.0" } [dev-dependencies] +test_utils = { path = "../test_utils" } walkdir = "2.3.1" rayon = "1" expect-test = "1.1" diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index b6c5de6588..70ba8adb48 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs @@ -91,6 +91,10 @@ pub fn path_from_segments( }) } +pub fn path_from_text(text: &str) -> ast::Path { + ast_from_text(&format!("fn main() {{ let test = {}; }}", text)) +} + pub fn glob_use_tree() -> ast::UseTree { ast_from_text("use *;") } diff --git a/crates/syntax/src/lib.rs b/crates/syntax/src/lib.rs index 11294c5b22..09e212e8c0 100644 --- a/crates/syntax/src/lib.rs +++ b/crates/syntax/src/lib.rs @@ -37,6 +37,7 @@ pub mod algo; pub mod ast; #[doc(hidden)] pub mod fuzz; +pub mod utils; use std::{marker::PhantomData, sync::Arc}; diff --git a/crates/syntax/src/utils.rs b/crates/syntax/src/utils.rs new file mode 100644 index 0000000000..f4c02518b4 --- /dev/null +++ b/crates/syntax/src/utils.rs @@ -0,0 +1,43 @@ +//! A set of utils methods to reuse on other abstraction levels + +use itertools::Itertools; + +use crate::{ast, match_ast, AstNode}; + +pub fn path_to_string_stripping_turbo_fish(path: &ast::Path) -> String { + path.syntax() + .children() + .filter_map(|node| { + match_ast! { + match node { + ast::PathSegment(it) => { + Some(it.name_ref()?.to_string()) + }, + ast::Path(it) => { + Some(path_to_string_stripping_turbo_fish(&it)) + }, + _ => None, + } + } + }) + .join("::") +} + +#[cfg(test)] +mod tests { + use super::path_to_string_stripping_turbo_fish; + use crate::ast::make; + + #[test] + fn turbofishes_are_stripped() { + assert_eq!("Vec", path_to_string_stripping_turbo_fish(&make::path_from_text("Vec::")),); + assert_eq!( + "Vec::new", + path_to_string_stripping_turbo_fish(&make::path_from_text("Vec::::new")), + ); + assert_eq!( + "Vec::new", + path_to_string_stripping_turbo_fish(&make::path_from_text("Vec::new()")), + ); + } +}