7873: Consider unresolved qualifiers during flyimport r=matklad a=SomeoneToIgnore

Closes https://github.com/rust-analyzer/rust-analyzer/issues/7679

Takes unresolved qualifiers into account, providing better completions (or none, if the path is resolved or do not match).

Does not handle cases when both path qualifier and some trait has to be imported: there are many extra issues with those (such as overlapping imports, for instance) that will require large diffs to address.

Also does not do a fuzzy search on qualifier, that requires some adjustments in `import_map` for better queries and changes to the default replace range which also seems relatively big to include here.

![qualifier_completion](https://user-images.githubusercontent.com/2690773/110040808-0af8dc00-7d4c-11eb-83db-65af94e843bb.gif)


7933: Improve compilation speed r=matklad a=matklad

bors r+
🤖

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
bors[bot] 2021-03-09 11:58:48 +00:00 committed by GitHub
commit 21913d0fdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 848 additions and 381 deletions

View file

@ -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<ItemInNs> 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 {

View file

@ -478,7 +478,6 @@ impl Analysis {
position: FilePosition,
full_import_path: &str,
imported_name: String,
import_for_trait_assoc_item: bool,
) -> Cancelable<Vec<TextEdit>> {
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())

View file

@ -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<u8>,
}
pub mod PubMod {
pub struct PubStruct1;
pub struct PubStruct2<T> {
_t: T,
}
}
",
r"
use PubMod::{PubStruct1, PubStruct2};
struct Test {
test: PubStruct2<u8>,
}
pub mod PubMod {
pub struct PubStruct1;
pub struct PubStruct2<T> {
_t: T,
}
}
",
);
}
#[test]
fn applicable_when_found_multiple_imports() {
check_assist(

View file

@ -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<hir::Trait> {
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),
}
}

View file

@ -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,13 +65,11 @@ 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<hir::ModuleDef, hir::MacroDef>| match candidate {
either::Either::Left(hir::ModuleDef::Trait(trait_)) => 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_| {

View file

@ -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
acc.add_all(
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)
.sorted_by_key(|located_import| {
compute_fuzzy_completion_order_key(
&located_import.import_path,
&user_input_lowercased,
)
})
.filter(|(_, proposed_def)| !scope_definitions.contains(proposed_def))
.collect::<Vec<_>>();
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)
}));
.filter_map(|import| {
render_resolution_with_import(
RenderContext::new(ctx),
ImportEdit { import, scope: import_scope.clone() },
)
}),
);
Some(())
}
fn scope_definitions(ctx: &CompletionContext) -> FxHashSet<ScopeDef> {
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<ImportAs
current_module,
ctx.sema.type_of_expr(dot_receiver)?,
fuzzy_name,
dot_receiver.syntax().clone(),
)
} else {
let fuzzy_name_length = fuzzy_name.len();
let approximate_node = match current_module.definition_source(ctx.db).value {
hir::ModuleSource::SourceFile(s) => 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![[]],
)
}
}

View file

@ -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 {
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()));
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 !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<CompletionItem> for Builder {
fn into(self) -> CompletionItem {
self.build()
}
}

View file

@ -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<Vec<TextEdit>> {
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)
let (import_path, item_to_import) =
items_locator::with_exact_name(&ctx.sema, current_crate, imported_name)
.into_iter()
.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)
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)?;
.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)]

View file

@ -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<CompletionItem> {
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| {
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.

View file

@ -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<Name> {
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");

View file

@ -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<ast::Path>,
/// Optional qualifier before name.
pub qualifier: Option<FirstSegmentUnresolved>,
/// 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<RootDatabase>,
) -> Option<Self> {
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<RootDatabase>,
) -> Option<Self> {
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<ast::Path>,
fuzzy_name: String,
sema: &Semantics<RootDatabase>,
candidate_node: SyntaxNode,
) -> Option<Self> {
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<Self> {
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<ModPath>,
}
impl LocatedImport {
pub fn new(
import_path: ModPath,
item_to_import: ItemInNs,
original_item: ItemInNs,
original_path: Option<ModPath>,
) -> 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<RootDatabase>,
prefix_kind: PrefixKind,
) -> Vec<(hir::ModPath, hir::ItemInNs)> {
) -> Vec<LocatedImport> {
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<RootDatabase>,
) -> Vec<(hir::ModPath, hir::ItemInNs)> {
pub fn search_for_relative_paths(&self, sema: &Semantics<RootDatabase>) -> Vec<LocatedImport> {
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<RootDatabase>,
prefixed: Option<hir::PrefixKind>,
) -> 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<PrefixKind>,
) -> Vec<LocatedImport> {
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(_) => {
let (assoc_item_search, limit) = if self.import_candidate.is_trait_candidate() {
(AssocItemSearch::AssocItemsOnly, None)
}
_ => (AssocItemSearch::Exclude, Some(DEFAULT_QUERY_SEARCH_LIMIT)),
} 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)
fn scope_definitions(&self, sema: &Semantics<RootDatabase>) -> FxHashSet<ScopeDef> {
let mut scope_definitions = FxHashSet::default();
sema.scope(&self.candidate_node).process_all_names(&mut |_, scope_def| {
scope_definitions.insert(scope_def);
});
scope_definitions
}
_ => None,
}?;
ModuleDef::from(canidate_trait).into()
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,
}
_ => item,
}
fn applicable_defs(
&self,
db: &RootDatabase,
prefixed: Option<PrefixKind>,
items_with_candidate_name: FxHashSet<ItemInNs>,
) -> FxHashSet<LocatedImport> {
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)
};
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)
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,
),
}
.map(|path| (path, item))
})
.filter(|(use_path, _)| use_path.len() > 1)
.collect::<Vec<_>>();
res.sort_by_cached_key(|(path, _)| path.clone());
res
}
}
fn applicable_defs<'a>(
import_candidate: &ImportCandidate,
current_crate: Crate,
fn path_applicable_imports(
db: &RootDatabase,
unfiltered_imports: Box<dyn Iterator<Item = Either<ModuleDef, MacroDef>> + 'a>,
) -> Box<dyn Iterator<Item = Either<ModuleDef, MacroDef>> + '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<ModPath> + Copy,
items_with_candidate_name: FxHashSet<ItemInNs>,
) -> FxHashSet<LocatedImport> {
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<ModPath>,
unresolved_first_segment: &str,
unresolved_qualifier: &str,
original_item: ItemInNs,
) -> Option<LocatedImport> {
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<ItemInNs> {
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<ItemInNs> {
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<Module> {
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<ModPath>,
items_with_candidate_name: FxHashSet<ItemInNs>,
) -> FxHashSet<LocatedImport> {
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<PrefixKind>,
) -> Option<ModPath> {
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()),
})
fn for_fuzzy_path(
qualifier: Option<ast::Path>,
fuzzy_name: String,
sema: &Semantics<RootDatabase>,
) -> Option<Self> {
path_import_candidate(sema, qualifier, NameToImport::Fuzzy(fuzzy_name))
}
_ => 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 is_trait_candidate(&self) -> bool {
matches!(self, ImportCandidate::TraitAssocItem(_) | ImportCandidate::TraitMethod(_))
}
}
fn path_import_candidate(
sema: &Semantics<RootDatabase>,
qualifier: Option<ast::Path>,
name: NameToImport,
) -> Option<ImportCandidate> {
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<AssocItem> {
item.as_module_def_id()
.and_then(|module_def_id| ModuleDef::from(module_def_id).as_assoc_item(db))
}

View file

@ -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<dyn Iterator<Item = Either<ModuleDef, MacroDef>>> {
exact_name: String,
) -> FxHashSet<ItemInNs> {
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<usize>,
) -> Box<dyn Iterator<Item = Either<ModuleDef, MacroDef>> + 'a> {
) -> FxHashSet<ItemInNs> {
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<ModuleDef, MacroDef>, 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<Item = Either<ModuleDef, MacroDef>> {
) -> FxHashSet<ItemInNs> {
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::<FxHashSet<_>>();
// 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<Definition> {
let _p = profile::span("get_name_definition");

View file

@ -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;

View file

@ -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(),
);

View file

@ -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"

View file

@ -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 *;")
}

View file

@ -37,6 +37,7 @@ pub mod algo;
pub mod ast;
#[doc(hidden)]
pub mod fuzz;
pub mod utils;
use std::{marker::PhantomData, sync::Arc};

View file

@ -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::<i32>")),);
assert_eq!(
"Vec::new",
path_to_string_stripping_turbo_fish(&make::path_from_text("Vec::<i32>::new")),
);
assert_eq!(
"Vec::new",
path_to_string_stripping_turbo_fish(&make::path_from_text("Vec::new()")),
);
}
}