//! module, and we use to statically check that we only produce snippet
//! assists if we are allowed to.
-use ide_db::helpers::{insert_use::MergeBehavior, SnippetCap};
+use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap};
use crate::AssistKind;
pub allowed: Option<Vec<AssistKind>>,
pub insert_use: InsertUseConfig,
}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub struct InsertUseConfig {
- pub merge: Option<MergeBehavior>,
- pub prefix_kind: hir::PrefixKind,
-}
use ide_db::helpers::{
+ import_assets::{ImportAssets, ImportCandidate},
insert_use::{insert_use, ImportScope},
mod_path_to_ast,
};
use syntax::ast;
-use crate::{
- utils::import_assets::{ImportAssets, ImportCandidate},
- AssistContext, AssistId, AssistKind, Assists, GroupLabel,
-};
+use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel};
// Feature: Auto Import
//
fn import_group_message(import_candidate: &ImportCandidate) -> GroupLabel {
let name = match import_candidate {
- ImportCandidate::UnqualifiedName(candidate)
- | ImportCandidate::QualifierStart(candidate) => format!("Import {}", &candidate.name),
+ ImportCandidate::Path(candidate) => format!("Import {}", &candidate.name),
ImportCandidate::TraitAssocItem(candidate) => {
format!("Import a trait for item {}", &candidate.name)
}
use std::iter;
use hir::AsName;
-use ide_db::helpers::mod_path_to_ast;
+use ide_db::helpers::{
+ import_assets::{ImportAssets, ImportCandidate},
+ mod_path_to_ast,
+};
use ide_db::RootDatabase;
use syntax::{
ast,
use crate::{
assist_context::{AssistContext, Assists},
- utils::import_assets::{ImportAssets, ImportCandidate},
AssistId, AssistKind, GroupLabel,
};
let range = ctx.sema.original_range(import_assets.syntax_under_caret()).range;
let qualify_candidate = match candidate {
- ImportCandidate::QualifierStart(_) => {
- mark::hit!(qualify_path_qualifier_start);
- let path = ast::Path::cast(import_assets.syntax_under_caret().clone())?;
- let (prev_segment, segment) = (path.qualifier()?.segment()?, path.segment()?);
- QualifyCandidate::QualifierStart(segment, prev_segment.generic_arg_list())
- }
- ImportCandidate::UnqualifiedName(_) => {
- mark::hit!(qualify_path_unqualified_name);
- let path = ast::Path::cast(import_assets.syntax_under_caret().clone())?;
- let generics = path.segment()?.generic_arg_list();
- QualifyCandidate::UnqualifiedName(generics)
+ ImportCandidate::Path(candidate) => {
+ if candidate.qualifier.is_some() {
+ mark::hit!(qualify_path_qualifier_start);
+ let path = ast::Path::cast(import_assets.syntax_under_caret().clone())?;
+ let (prev_segment, segment) = (path.qualifier()?.segment()?, path.segment()?);
+ QualifyCandidate::QualifierStart(segment, prev_segment.generic_arg_list())
+ } else {
+ mark::hit!(qualify_path_unqualified_name);
+ let path = ast::Path::cast(import_assets.syntax_under_caret().clone())?;
+ let generics = path.segment()?.generic_arg_list();
+ QualifyCandidate::UnqualifiedName(generics)
+ }
}
ImportCandidate::TraitAssocItem(_) => {
mark::hit!(qualify_path_trait_assoc_item);
fn group_label(candidate: &ImportCandidate) -> GroupLabel {
let name = match candidate {
- ImportCandidate::UnqualifiedName(it) | ImportCandidate::QualifierStart(it) => &it.name,
+ ImportCandidate::Path(it) => &it.name,
ImportCandidate::TraitAssocItem(it) | ImportCandidate::TraitMethod(it) => &it.name,
};
GroupLabel(format!("Qualify {}", name))
fn label(candidate: &ImportCandidate, import: &hir::ModPath) -> String {
match candidate {
- ImportCandidate::UnqualifiedName(_) => format!("Qualify as `{}`", &import),
- ImportCandidate::QualifierStart(_) => format!("Qualify with `{}`", &import),
+ ImportCandidate::Path(candidate) => {
+ if candidate.qualifier.is_some() {
+ format!("Qualify with `{}`", &import)
+ } else {
+ format!("Qualify as `{}`", &import)
+ }
+ }
ImportCandidate::TraitAssocItem(_) => format!("Qualify `{}`", &import),
ImportCandidate::TraitMethod(_) => format!("Qualify with cast as `{}`", &import),
}
pub(crate) use crate::assist_context::{AssistContext, Assists};
-pub use assist_config::{AssistConfig, InsertUseConfig};
+pub use assist_config::AssistConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssistKind {
use hir::Semantics;
use ide_db::{
base_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt},
- helpers::{insert_use::MergeBehavior, SnippetCap},
+ helpers::{
+ insert_use::{InsertUseConfig, MergeBehavior},
+ SnippetCap,
+ },
source_change::FileSystemEdit,
RootDatabase,
};
use syntax::TextRange;
use test_utils::{assert_eq_text, extract_offset, extract_range};
-use crate::{
- handlers::Handler, Assist, AssistConfig, AssistContext, AssistKind, Assists, InsertUseConfig,
-};
+use crate::{handlers::Handler, Assist, AssistConfig, AssistContext, AssistKind, Assists};
use stdx::{format_to, trim_indent};
pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig {
//! Assorted functions shared by several assists.
-pub(crate) mod import_assets;
use std::ops;
+++ /dev/null
-//! Look up accessible paths for items.
-use either::Either;
-use hir::{AsAssocItem, AssocItemContainer, ModuleDef, Semantics};
-use ide_db::{imports_locator, RootDatabase};
-use rustc_hash::FxHashSet;
-use syntax::{ast, AstNode, SyntaxNode};
-
-use crate::assist_config::InsertUseConfig;
-
-#[derive(Debug)]
-pub(crate) enum ImportCandidate {
- /// Simple name like 'HashMap'
- UnqualifiedName(PathImportCandidate),
- /// First part of the qualified name.
- /// For 'std::collections::HashMap', that will be 'std'.
- QualifierStart(PathImportCandidate),
- /// A trait associated function (with no self parameter) or associated constant.
- /// For 'test_mod::TestEnum::test_function', `ty` is the `test_mod::TestEnum` expression type
- /// and `name` is the `test_function`
- TraitAssocItem(TraitImportCandidate),
- /// A trait method with self parameter.
- /// For 'test_enum.test_method()', `ty` is the `test_enum` expression type
- /// and `name` is the `test_method`
- TraitMethod(TraitImportCandidate),
-}
-
-#[derive(Debug)]
-pub(crate) struct TraitImportCandidate {
- pub(crate) ty: hir::Type,
- pub(crate) name: ast::NameRef,
-}
-
-#[derive(Debug)]
-pub(crate) struct PathImportCandidate {
- pub(crate) name: ast::NameRef,
-}
-
-#[derive(Debug)]
-pub(crate) struct ImportAssets {
- import_candidate: ImportCandidate,
- module_with_name_to_import: hir::Module,
- syntax_under_caret: SyntaxNode,
-}
-
-impl ImportAssets {
- pub(crate) fn for_method_call(
- method_call: ast::MethodCallExpr,
- sema: &Semantics<RootDatabase>,
- ) -> Option<Self> {
- let syntax_under_caret = method_call.syntax().to_owned();
- let module_with_name_to_import = sema.scope(&syntax_under_caret).module()?;
- Some(Self {
- import_candidate: ImportCandidate::for_method_call(sema, &method_call)?,
- module_with_name_to_import,
- syntax_under_caret,
- })
- }
-
- pub(crate) fn for_regular_path(
- path_under_caret: ast::Path,
- sema: &Semantics<RootDatabase>,
- ) -> Option<Self> {
- let syntax_under_caret = path_under_caret.syntax().to_owned();
- if syntax_under_caret.ancestors().find_map(ast::Use::cast).is_some() {
- return None;
- }
-
- let module_with_name_to_import = sema.scope(&syntax_under_caret).module()?;
- Some(Self {
- import_candidate: ImportCandidate::for_regular_path(sema, &path_under_caret)?,
- module_with_name_to_import,
- syntax_under_caret,
- })
- }
-
- pub(crate) fn syntax_under_caret(&self) -> &SyntaxNode {
- &self.syntax_under_caret
- }
-
- pub(crate) fn import_candidate(&self) -> &ImportCandidate {
- &self.import_candidate
- }
-
- fn get_search_query(&self) -> &str {
- match &self.import_candidate {
- ImportCandidate::UnqualifiedName(candidate)
- | ImportCandidate::QualifierStart(candidate) => candidate.name.text(),
- ImportCandidate::TraitAssocItem(candidate)
- | ImportCandidate::TraitMethod(candidate) => candidate.name.text(),
- }
- }
-
- pub(crate) fn search_for_imports(
- &self,
- sema: &Semantics<RootDatabase>,
- config: &InsertUseConfig,
- ) -> Vec<(hir::ModPath, hir::ItemInNs)> {
- let _p = profile::span("import_assists::search_for_imports");
- self.search_for(sema, Some(config.prefix_kind))
- }
-
- /// This may return non-absolute paths if a part of the returned path is already imported into scope.
- #[allow(dead_code)]
- pub(crate) fn search_for_relative_paths(
- &self,
- sema: &Semantics<RootDatabase>,
- ) -> Vec<(hir::ModPath, hir::ItemInNs)> {
- let _p = profile::span("import_assists::search_for_relative_paths");
- self.search_for(sema, None)
- }
-
- fn search_for(
- &self,
- sema: &Semantics<RootDatabase>,
- prefixed: Option<hir::PrefixKind>,
- ) -> Vec<(hir::ModPath, hir::ItemInNs)> {
- let db = sema.db;
- let mut trait_candidates = FxHashSet::default();
- let current_crate = self.module_with_name_to_import.krate();
-
- let filter = |candidate: Either<hir::ModuleDef, hir::MacroDef>| {
- trait_candidates.clear();
- match &self.import_candidate {
- ImportCandidate::TraitAssocItem(trait_candidate) => {
- let located_assoc_item = match candidate {
- Either::Left(ModuleDef::Function(located_function)) => {
- located_function.as_assoc_item(db)
- }
- Either::Left(ModuleDef::Const(located_const)) => {
- located_const.as_assoc_item(db)
- }
- _ => None,
- }
- .map(|assoc| assoc.container(db))
- .and_then(Self::assoc_to_trait)?;
-
- trait_candidates.insert(located_assoc_item.into());
-
- trait_candidate
- .ty
- .iterate_path_candidates(
- db,
- current_crate,
- &trait_candidates,
- None,
- |_, assoc| Self::assoc_to_trait(assoc.container(db)),
- )
- .map(ModuleDef::from)
- .map(Either::Left)
- }
- ImportCandidate::TraitMethod(trait_candidate) => {
- let located_assoc_item =
- if let Either::Left(ModuleDef::Function(located_function)) = candidate {
- located_function
- .as_assoc_item(db)
- .map(|assoc| assoc.container(db))
- .and_then(Self::assoc_to_trait)
- } else {
- None
- }?;
-
- trait_candidates.insert(located_assoc_item.into());
-
- trait_candidate
- .ty
- .iterate_method_candidates(
- db,
- current_crate,
- &trait_candidates,
- None,
- |_, function| {
- Self::assoc_to_trait(function.as_assoc_item(db)?.container(db))
- },
- )
- .map(ModuleDef::from)
- .map(Either::Left)
- }
- _ => Some(candidate),
- }
- };
-
- let mut res = imports_locator::find_exact_imports(
- sema,
- current_crate,
- self.get_search_query().to_string(),
- )
- .filter_map(filter)
- .filter_map(|candidate| {
- let item: hir::ItemInNs = candidate.either(Into::into, Into::into);
- if let Some(prefix_kind) = prefixed {
- self.module_with_name_to_import.find_use_path_prefixed(db, item, prefix_kind)
- } else {
- self.module_with_name_to_import.find_use_path(db, item)
- }
- .map(|path| (path, item))
- })
- .filter(|(use_path, _)| use_path.len() > 1)
- .take(20)
- .collect::<Vec<_>>();
- res.sort_by_key(|(path, _)| path.clone());
- res
- }
-
- fn assoc_to_trait(assoc: AssocItemContainer) -> Option<hir::Trait> {
- if let AssocItemContainer::Trait(extracted_trait) = assoc {
- Some(extracted_trait)
- } else {
- None
- }
- }
-}
-
-impl ImportCandidate {
- fn for_method_call(
- sema: &Semantics<RootDatabase>,
- method_call: &ast::MethodCallExpr,
- ) -> Option<Self> {
- match sema.resolve_method_call(method_call) {
- Some(_) => None,
- None => Some(Self::TraitMethod(TraitImportCandidate {
- ty: sema.type_of_expr(&method_call.receiver()?)?,
- name: method_call.name_ref()?,
- })),
- }
- }
-
- fn for_regular_path(
- sema: &Semantics<RootDatabase>,
- path_under_caret: &ast::Path,
- ) -> Option<Self> {
- if sema.resolve_path(path_under_caret).is_some() {
- return None;
- }
-
- let segment = path_under_caret.segment()?;
- let candidate = if let Some(qualifier) = path_under_caret.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 {
- ty: assoc_item_path.ty(sema.db),
- name: segment.name_ref()?,
- })
- }
- _ => return None,
- }
- } else {
- ImportCandidate::QualifierStart(PathImportCandidate { name: qualifier_start })
- }
- } else {
- ImportCandidate::UnqualifiedName(PathImportCandidate {
- name: segment.syntax().descendants().find_map(ast::NameRef::cast)?,
- })
- };
- Some(candidate)
- }
-}
pub(crate) mod macro_in_item_position;
pub(crate) mod trait_impl;
pub(crate) mod mod_;
+pub(crate) mod flyimport;
use hir::{ModPath, ScopeDef, Type};
--- /dev/null
+//! Feature: completion with imports-on-the-fly
+//!
+//! When completing names in the current scope, proposes additional imports from other modules or crates,
+//! if they can be qualified in the scope and their name contains all symbols from the completion input
+//! (case-insensitive, in any order or places).
+//!
+//! ```
+//! fn main() {
+//! pda$0
+//! }
+//! # pub mod std { pub mod marker { pub struct PhantomData { } } }
+//! ```
+//! ->
+//! ```
+//! use std::marker::PhantomData;
+//!
+//! fn main() {
+//! PhantomData
+//! }
+//! # pub mod std { pub mod marker { pub struct PhantomData { } } }
+//! ```
+//!
+//! .Fuzzy search details
+//!
+//! To avoid an excessive amount of the results returned, completion input is checked for inclusion in the names only
+//! (i.e. in `HashMap` in the `std::collections::HashMap` path).
+//! For the same reasons, avoids searching for any imports for inputs with their length less that 2 symbols.
+//!
+//! .Import configuration
+//!
+//! It is possible to configure how use-trees are merged with the `importMergeBehavior` setting.
+//! Mimics the corresponding behavior of the `Auto Import` feature.
+//!
+//! .LSP and performance implications
+//!
+//! The feature is enabled only if the LSP client supports LSP protocol version 3.16+ and reports the `additionalTextEdits`
+//! (case sensitive) resolve client capability in its client capabilities.
+//! This way the server is able to defer the costly computations, doing them for a selected completion item only.
+//! For clients with no such support, all edits have to be calculated on the completion request, including the fuzzy search completion ones,
+//! which might be slow ergo the feature is automatically disabled.
+//!
+//! .Feature toggle
+//!
+//! The feature can be forcefully turned off in the settings with the `rust-analyzer.completion.enableAutoimportCompletions` flag.
+//! 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 either::Either;
+use hir::{ModPath, ScopeDef};
+use ide_db::{helpers::insert_use::ImportScope, imports_locator};
+use syntax::AstNode;
+use test_utils::mark;
+
+use crate::{
+ context::CompletionContext,
+ render::{render_resolution_with_import, RenderContext},
+ ImportEdit,
+};
+
+use super::Completions;
+
+pub(crate) fn import_on_the_fly(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
+ if !ctx.config.enable_autoimport_completions {
+ return None;
+ }
+ if ctx.attribute_under_caret.is_some() || ctx.mod_declaration_under_caret.is_some() {
+ return None;
+ }
+ let potential_import_name = ctx.token.to_string();
+ if potential_import_name.len() < 2 {
+ return None;
+ }
+ let _p = profile::span("import_on_the_fly").detail(|| potential_import_name.to_string());
+
+ let current_module = ctx.scope.module()?;
+ let anchor = ctx.name_ref_syntax.as_ref()?;
+ let import_scope = ImportScope::find_insert_use_container(anchor.syntax(), &ctx.sema)?;
+
+ let user_input_lowercased = potential_import_name.to_lowercase();
+ let mut all_mod_paths = imports_locator::find_similar_imports(
+ &ctx.sema,
+ ctx.krate?,
+ Some(40),
+ potential_import_name,
+ true,
+ true,
+ )
+ .filter_map(|import_candidate| {
+ Some(match import_candidate {
+ Either::Left(module_def) => {
+ (current_module.find_use_path(ctx.db, module_def)?, ScopeDef::ModuleDef(module_def))
+ }
+ Either::Right(macro_def) => {
+ (current_module.find_use_path(ctx.db, macro_def)?, ScopeDef::MacroDef(macro_def))
+ }
+ })
+ })
+ .filter(|(mod_path, _)| mod_path.len() > 1)
+ .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)| {
+ render_resolution_with_import(
+ RenderContext::new(ctx),
+ ImportEdit { import_path, import_scope: import_scope.clone() },
+ &definition,
+ )
+ }));
+ Some(())
+}
+
+fn compute_fuzzy_completion_order_key(
+ proposed_mod_path: &ModPath,
+ user_input_lowercased: &str,
+) -> usize {
+ mark::hit!(certain_fuzzy_order_test);
+ let proposed_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() {
+ Some((first_matching_index, _)) => first_matching_index,
+ None => usize::MAX,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use expect_test::{expect, Expect};
+ use test_utils::mark;
+
+ use crate::{
+ item::CompletionKind,
+ test_utils::{check_edit, completion_list},
+ };
+
+ fn check(ra_fixture: &str, expect: Expect) {
+ let actual = completion_list(ra_fixture, CompletionKind::Magic);
+ expect.assert_eq(&actual);
+ }
+
+ #[test]
+ fn function_fuzzy_completion() {
+ check_edit(
+ "stdin",
+ r#"
+//- /lib.rs crate:dep
+pub mod io {
+ pub fn stdin() {}
+};
+
+//- /main.rs crate:main deps:dep
+fn main() {
+ stdi$0
+}
+"#,
+ r#"
+use dep::io::stdin;
+
+fn main() {
+ stdin()$0
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn macro_fuzzy_completion() {
+ check_edit(
+ "macro_with_curlies!",
+ r#"
+//- /lib.rs crate:dep
+/// Please call me as macro_with_curlies! {}
+#[macro_export]
+macro_rules! macro_with_curlies {
+ () => {}
+}
+
+//- /main.rs crate:main deps:dep
+fn main() {
+ curli$0
+}
+"#,
+ r#"
+use dep::macro_with_curlies;
+
+fn main() {
+ macro_with_curlies! {$0}
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn struct_fuzzy_completion() {
+ check_edit(
+ "ThirdStruct",
+ r#"
+//- /lib.rs crate:dep
+pub struct FirstStruct;
+pub mod some_module {
+ pub struct SecondStruct;
+ pub struct ThirdStruct;
+}
+
+//- /main.rs crate:main deps:dep
+use dep::{FirstStruct, some_module::SecondStruct};
+
+fn main() {
+ this$0
+}
+"#,
+ r#"
+use dep::{FirstStruct, some_module::{SecondStruct, ThirdStruct}};
+
+fn main() {
+ ThirdStruct
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn fuzzy_completions_come_in_specific_order() {
+ mark::check!(certain_fuzzy_order_test);
+ check(
+ r#"
+//- /lib.rs crate:dep
+pub struct FirstStruct;
+pub mod some_module {
+ // already imported, omitted
+ pub struct SecondStruct;
+ // does not contain all letters from the query, omitted
+ pub struct UnrelatedOne;
+ // contains all letters from the query, but not in sequence, displayed last
+ pub struct ThiiiiiirdStruct;
+ // contains all letters from the query, but not in the beginning, displayed second
+ pub struct AfterThirdStruct;
+ // contains all letters from the query in the begginning, displayed first
+ pub struct ThirdStruct;
+}
+
+//- /main.rs crate:main deps:dep
+use dep::{FirstStruct, some_module::SecondStruct};
+
+fn main() {
+ hir$0
+}
+"#,
+ expect![[r#"
+ st dep::some_module::ThirdStruct
+ st dep::some_module::AfterThirdStruct
+ st dep::some_module::ThiiiiiirdStruct
+ "#]],
+ );
+ }
+
+ #[test]
+ fn does_not_propose_names_in_scope() {
+ check(
+ r#"
+ //- /lib.rs crate:dep
+ pub mod test_mod {
+ pub trait TestTrait {
+ const SPECIAL_CONST: u8;
+ type HumbleType;
+ fn weird_function();
+ fn random_method(&self);
+ }
+ pub struct TestStruct {}
+ impl TestTrait for TestStruct {
+ const SPECIAL_CONST: u8 = 42;
+ type HumbleType = ();
+ fn weird_function() {}
+ fn random_method(&self) {}
+ }
+ }
+
+ //- /main.rs crate:main deps:dep
+ use dep::test_mod::TestStruct;
+ fn main() {
+ TestSt$0
+ }
+ "#,
+ expect![[r#""#]],
+ );
+ }
+}
use std::iter;
-use either::Either;
-use hir::{Adt, ModPath, ModuleDef, ScopeDef, Type};
-use ide_db::helpers::insert_use::ImportScope;
-use ide_db::imports_locator;
+use hir::{Adt, ModuleDef, ScopeDef, Type};
use syntax::AstNode;
use test_utils::mark;
-use crate::{
- render::{render_resolution_with_import, RenderContext},
- CompletionContext, Completions, ImportEdit,
-};
+use crate::{CompletionContext, Completions};
pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionContext) {
if !(ctx.is_trivial_path || ctx.is_pat_binding_or_const) {
}
acc.add_resolution(ctx, name.to_string(), &res)
});
-
- if ctx.config.enable_autoimport_completions {
- fuzzy_completion(acc, ctx);
- }
}
fn complete_enum_variants(acc: &mut Completions, ctx: &CompletionContext, ty: &Type) {
}
}
-// Feature: Fuzzy Completion and Autoimports
-//
-// When completing names in the current scope, proposes additional imports from other modules or crates,
-// if they can be qualified in the scope and their name contains all symbols from the completion input
-// (case-insensitive, in any order or places).
-//
-// ```
-// fn main() {
-// pda$0
-// }
-// # pub mod std { pub mod marker { pub struct PhantomData { } } }
-// ```
-// ->
-// ```
-// use std::marker::PhantomData;
-//
-// fn main() {
-// PhantomData
-// }
-// # pub mod std { pub mod marker { pub struct PhantomData { } } }
-// ```
-//
-// .Fuzzy search details
-//
-// To avoid an excessive amount of the results returned, completion input is checked for inclusion in the names only
-// (i.e. in `HashMap` in the `std::collections::HashMap` path).
-// For the same reasons, avoids searching for any imports for inputs with their length less that 2 symbols.
-//
-// .Merge Behavior
-//
-// It is possible to configure how use-trees are merged with the `importMergeBehavior` setting.
-// Mimics the corresponding behavior of the `Auto Import` feature.
-//
-// .LSP and performance implications
-//
-// The feature is enabled only if the LSP client supports LSP protocol version 3.16+ and reports the `additionalTextEdits`
-// (case sensitive) resolve client capability in its client capabilities.
-// This way the server is able to defer the costly computations, doing them for a selected completion item only.
-// For clients with no such support, all edits have to be calculated on the completion request, including the fuzzy search completion ones,
-// which might be slow ergo the feature is automatically disabled.
-//
-// .Feature toggle
-//
-// The feature can be forcefully turned off in the settings with the `rust-analyzer.completion.enableAutoimportCompletions` flag.
-// 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.
-fn fuzzy_completion(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
- let potential_import_name = ctx.token.to_string();
- let _p = profile::span("fuzzy_completion").detail(|| potential_import_name.clone());
-
- if potential_import_name.len() < 2 {
- return None;
- }
-
- let current_module = ctx.scope.module()?;
- let anchor = ctx.name_ref_syntax.as_ref()?;
- let import_scope = ImportScope::find_insert_use_container(anchor.syntax(), &ctx.sema)?;
-
- let user_input_lowercased = potential_import_name.to_lowercase();
- let mut all_mod_paths = imports_locator::find_similar_imports(
- &ctx.sema,
- ctx.krate?,
- Some(40),
- potential_import_name,
- true,
- true,
- )
- .filter_map(|import_candidate| {
- Some(match import_candidate {
- Either::Left(module_def) => {
- (current_module.find_use_path(ctx.db, module_def)?, ScopeDef::ModuleDef(module_def))
- }
- Either::Right(macro_def) => {
- (current_module.find_use_path(ctx.db, macro_def)?, ScopeDef::MacroDef(macro_def))
- }
- })
- })
- .filter(|(mod_path, _)| mod_path.len() > 1)
- .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)| {
- render_resolution_with_import(
- RenderContext::new(ctx),
- ImportEdit { import_path, import_scope: import_scope.clone() },
- &definition,
- )
- }));
- Some(())
-}
-
-fn compute_fuzzy_completion_order_key(
- proposed_mod_path: &ModPath,
- user_input_lowercased: &str,
-) -> usize {
- mark::hit!(certain_fuzzy_order_test);
- let proposed_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() {
- Some((first_matching_index, _)) => first_matching_index,
- None => usize::MAX,
- }
-}
-
#[cfg(test)]
mod tests {
use expect_test::{expect, Expect};
use test_utils::mark;
use crate::{
- test_utils::{
- check_edit, check_edit_with_config, completion_list_with_config, TEST_CONFIG,
- },
+ test_utils::{check_edit, completion_list_with_config, TEST_CONFIG},
CompletionConfig, CompletionKind,
};
"#]],
)
}
-
- #[test]
- fn function_fuzzy_completion() {
- check_edit_with_config(
- TEST_CONFIG,
- "stdin",
- r#"
-//- /lib.rs crate:dep
-pub mod io {
- pub fn stdin() {}
-};
-
-//- /main.rs crate:main deps:dep
-fn main() {
- stdi$0
-}
-"#,
- r#"
-use dep::io::stdin;
-
-fn main() {
- stdin()$0
-}
-"#,
- );
- }
-
- #[test]
- fn macro_fuzzy_completion() {
- check_edit_with_config(
- TEST_CONFIG,
- "macro_with_curlies!",
- r#"
-//- /lib.rs crate:dep
-/// Please call me as macro_with_curlies! {}
-#[macro_export]
-macro_rules! macro_with_curlies {
- () => {}
-}
-
-//- /main.rs crate:main deps:dep
-fn main() {
- curli$0
-}
-"#,
- r#"
-use dep::macro_with_curlies;
-
-fn main() {
- macro_with_curlies! {$0}
-}
-"#,
- );
- }
-
- #[test]
- fn struct_fuzzy_completion() {
- check_edit_with_config(
- TEST_CONFIG,
- "ThirdStruct",
- r#"
-//- /lib.rs crate:dep
-pub struct FirstStruct;
-pub mod some_module {
- pub struct SecondStruct;
- pub struct ThirdStruct;
-}
-
-//- /main.rs crate:main deps:dep
-use dep::{FirstStruct, some_module::SecondStruct};
-
-fn main() {
- this$0
-}
-"#,
- r#"
-use dep::{FirstStruct, some_module::{SecondStruct, ThirdStruct}};
-
-fn main() {
- ThirdStruct
-}
-"#,
- );
- }
-
- #[test]
- fn fuzzy_completions_come_in_specific_order() {
- mark::check!(certain_fuzzy_order_test);
- check_with_config(
- TEST_CONFIG,
- r#"
-//- /lib.rs crate:dep
-pub struct FirstStruct;
-pub mod some_module {
- // already imported, omitted
- pub struct SecondStruct;
- // does not contain all letters from the query, omitted
- pub struct UnrelatedOne;
- // contains all letters from the query, but not in sequence, displayed last
- pub struct ThiiiiiirdStruct;
- // contains all letters from the query, but not in the beginning, displayed second
- pub struct AfterThirdStruct;
- // contains all letters from the query in the begginning, displayed first
- pub struct ThirdStruct;
-}
-
-//- /main.rs crate:main deps:dep
-use dep::{FirstStruct, some_module::SecondStruct};
-
-fn main() {
- hir$0
-}
-"#,
- expect![[r#"
- fn main() fn main()
- st SecondStruct
- st FirstStruct
- md dep
- st dep::some_module::ThirdStruct
- st dep::some_module::AfterThirdStruct
- st dep::some_module::ThiiiiiirdStruct
- "#]],
- );
- }
}
//! module, and we use to statically check that we only produce snippet
//! completions if we are allowed to.
-use ide_db::helpers::{insert_use::MergeBehavior, SnippetCap};
+use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompletionConfig {
pub add_call_parenthesis: bool,
pub add_call_argument_snippets: bool,
pub snippet_cap: Option<SnippetCap>,
- pub merge: Option<MergeBehavior>,
+ pub insert_use: InsertUseConfig,
}
completions::macro_in_item_position::complete_macro_in_item_position(&mut acc, &ctx);
completions::trait_impl::complete_trait_impl(&mut acc, &ctx);
completions::mod_::complete_mod(&mut acc, &ctx);
+ completions::flyimport::import_on_the_fly(&mut acc, &ctx);
Some(acc)
}
})
.find(|mod_path| mod_path.to_string() == full_import_path)?;
- ImportEdit { import_path, import_scope }.to_text_edit(config.merge).map(|edit| vec![edit])
+ ImportEdit { import_path, import_scope }
+ .to_text_edit(config.insert_use.merge)
+ .map(|edit| vec![edit])
}
#[cfg(test)]
//! Runs completion for testing purposes.
-use hir::Semantics;
+use hir::{PrefixKind, Semantics};
use ide_db::{
base_db::{fixture::ChangeFixture, FileLoader, FilePosition},
- helpers::{insert_use::MergeBehavior, SnippetCap},
+ helpers::{
+ insert_use::{InsertUseConfig, MergeBehavior},
+ SnippetCap,
+ },
RootDatabase,
};
use itertools::Itertools;
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: SnippetCap::new(true),
- merge: Some(MergeBehavior::Full),
+ insert_use: InsertUseConfig {
+ merge: Some(MergeBehavior::Full),
+ prefix_kind: PrefixKind::Plain,
+ },
};
/// Creates analysis from a multi-file fixture, returns positions marked with $0.
let mut combined_edit = completion.text_edit().to_owned();
if let Some(import_text_edit) =
- completion.import_to_add().and_then(|edit| edit.to_text_edit(config.merge))
+ completion.import_to_add().and_then(|edit| edit.to_text_edit(config.insert_use.merge))
{
combined_edit.union(import_text_edit).expect(
"Failed to apply completion resolve changes: change ranges overlap, but should not",
HlRange,
},
};
-pub use assists::{Assist, AssistConfig, AssistId, AssistKind, InsertUseConfig};
+pub use assists::{Assist, AssistConfig, AssistId, AssistKind};
pub use completion::{
CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, ImportEdit,
InsertTextFormat,
//! A module with ide helpers for high-level ide features.
pub mod insert_use;
+pub mod import_assets;
use hir::{Crate, Enum, Module, ScopeDef, Semantics, Trait};
use syntax::ast::{self, make};
--- /dev/null
+//! Look up accessible paths for items.
+use either::Either;
+use hir::{AsAssocItem, AssocItemContainer, ModuleDef, Semantics};
+use rustc_hash::FxHashSet;
+use syntax::{ast, AstNode, SyntaxNode};
+
+use crate::{imports_locator, RootDatabase};
+
+use super::insert_use::InsertUseConfig;
+
+#[derive(Debug)]
+pub enum ImportCandidate {
+ // A path, qualified (`std::collections::HashMap`) or not (`HashMap`).
+ Path(PathImportCandidate),
+ /// A trait associated function (with no self parameter) or associated constant.
+ /// For 'test_mod::TestEnum::test_function', `ty` is the `test_mod::TestEnum` expression type
+ /// and `name` is the `test_function`
+ TraitAssocItem(TraitImportCandidate),
+ /// A trait method with self parameter.
+ /// For 'test_enum.test_method()', `ty` is the `test_enum` expression type
+ /// and `name` is the `test_method`
+ TraitMethod(TraitImportCandidate),
+}
+
+#[derive(Debug)]
+pub struct TraitImportCandidate {
+ pub ty: hir::Type,
+ pub name: ast::NameRef,
+}
+
+#[derive(Debug)]
+pub struct PathImportCandidate {
+ pub qualifier: Option<ast::Path>,
+ pub name: ast::NameRef,
+}
+
+#[derive(Debug)]
+pub struct ImportAssets {
+ import_candidate: ImportCandidate,
+ module_with_name_to_import: hir::Module,
+ syntax_under_caret: SyntaxNode,
+}
+
+impl ImportAssets {
+ pub fn for_method_call(
+ method_call: ast::MethodCallExpr,
+ sema: &Semantics<RootDatabase>,
+ ) -> Option<Self> {
+ let syntax_under_caret = method_call.syntax().to_owned();
+ let module_with_name_to_import = sema.scope(&syntax_under_caret).module()?;
+ Some(Self {
+ import_candidate: ImportCandidate::for_method_call(sema, &method_call)?,
+ module_with_name_to_import,
+ syntax_under_caret,
+ })
+ }
+
+ pub fn for_regular_path(
+ path_under_caret: ast::Path,
+ sema: &Semantics<RootDatabase>,
+ ) -> Option<Self> {
+ let syntax_under_caret = path_under_caret.syntax().to_owned();
+ if syntax_under_caret.ancestors().find_map(ast::Use::cast).is_some() {
+ return None;
+ }
+
+ let module_with_name_to_import = sema.scope(&syntax_under_caret).module()?;
+ Some(Self {
+ import_candidate: ImportCandidate::for_regular_path(sema, &path_under_caret)?,
+ module_with_name_to_import,
+ syntax_under_caret,
+ })
+ }
+
+ pub fn syntax_under_caret(&self) -> &SyntaxNode {
+ &self.syntax_under_caret
+ }
+
+ pub fn import_candidate(&self) -> &ImportCandidate {
+ &self.import_candidate
+ }
+
+ fn get_search_query(&self) -> &str {
+ match &self.import_candidate {
+ ImportCandidate::Path(candidate) => candidate.name.text(),
+ ImportCandidate::TraitAssocItem(candidate)
+ | ImportCandidate::TraitMethod(candidate) => candidate.name.text(),
+ }
+ }
+
+ pub fn search_for_imports(
+ &self,
+ sema: &Semantics<RootDatabase>,
+ config: &InsertUseConfig,
+ ) -> Vec<(hir::ModPath, hir::ItemInNs)> {
+ let _p = profile::span("import_assists::search_for_imports");
+ self.search_for(sema, Some(config.prefix_kind))
+ }
+
+ /// This may return non-absolute paths if a part of the returned path is already imported into scope.
+ #[allow(dead_code)]
+ pub fn search_for_relative_paths(
+ &self,
+ sema: &Semantics<RootDatabase>,
+ ) -> Vec<(hir::ModPath, hir::ItemInNs)> {
+ let _p = profile::span("import_assists::search_for_relative_paths");
+ self.search_for(sema, None)
+ }
+
+ fn search_for(
+ &self,
+ sema: &Semantics<RootDatabase>,
+ prefixed: Option<hir::PrefixKind>,
+ ) -> Vec<(hir::ModPath, hir::ItemInNs)> {
+ let db = sema.db;
+ let mut trait_candidates = FxHashSet::default();
+ let current_crate = self.module_with_name_to_import.krate();
+
+ let filter = |candidate: Either<hir::ModuleDef, hir::MacroDef>| {
+ trait_candidates.clear();
+ match &self.import_candidate {
+ ImportCandidate::TraitAssocItem(trait_candidate) => {
+ let located_assoc_item = match candidate {
+ Either::Left(ModuleDef::Function(located_function)) => {
+ located_function.as_assoc_item(db)
+ }
+ Either::Left(ModuleDef::Const(located_const)) => {
+ located_const.as_assoc_item(db)
+ }
+ _ => None,
+ }
+ .map(|assoc| assoc.container(db))
+ .and_then(Self::assoc_to_trait)?;
+
+ trait_candidates.insert(located_assoc_item.into());
+
+ trait_candidate
+ .ty
+ .iterate_path_candidates(
+ db,
+ current_crate,
+ &trait_candidates,
+ None,
+ |_, assoc| Self::assoc_to_trait(assoc.container(db)),
+ )
+ .map(ModuleDef::from)
+ .map(Either::Left)
+ }
+ ImportCandidate::TraitMethod(trait_candidate) => {
+ let located_assoc_item =
+ if let Either::Left(ModuleDef::Function(located_function)) = candidate {
+ located_function
+ .as_assoc_item(db)
+ .map(|assoc| assoc.container(db))
+ .and_then(Self::assoc_to_trait)
+ } else {
+ None
+ }?;
+
+ trait_candidates.insert(located_assoc_item.into());
+
+ trait_candidate
+ .ty
+ .iterate_method_candidates(
+ db,
+ current_crate,
+ &trait_candidates,
+ None,
+ |_, function| {
+ Self::assoc_to_trait(function.as_assoc_item(db)?.container(db))
+ },
+ )
+ .map(ModuleDef::from)
+ .map(Either::Left)
+ }
+ _ => Some(candidate),
+ }
+ };
+
+ let mut res = imports_locator::find_exact_imports(
+ sema,
+ current_crate,
+ self.get_search_query().to_string(),
+ )
+ .filter_map(filter)
+ .filter_map(|candidate| {
+ let item: hir::ItemInNs = candidate.either(Into::into, Into::into);
+ if let Some(prefix_kind) = prefixed {
+ self.module_with_name_to_import.find_use_path_prefixed(db, item, prefix_kind)
+ } else {
+ self.module_with_name_to_import.find_use_path(db, item)
+ }
+ .map(|path| (path, item))
+ })
+ .filter(|(use_path, _)| use_path.len() > 1)
+ .take(20)
+ .collect::<Vec<_>>();
+ res.sort_by_key(|(path, _)| path.clone());
+ res
+ }
+
+ fn assoc_to_trait(assoc: AssocItemContainer) -> Option<hir::Trait> {
+ if let AssocItemContainer::Trait(extracted_trait) = assoc {
+ Some(extracted_trait)
+ } else {
+ None
+ }
+ }
+}
+
+impl ImportCandidate {
+ fn for_method_call(
+ sema: &Semantics<RootDatabase>,
+ method_call: &ast::MethodCallExpr,
+ ) -> Option<Self> {
+ match sema.resolve_method_call(method_call) {
+ Some(_) => None,
+ None => Some(Self::TraitMethod(TraitImportCandidate {
+ ty: sema.type_of_expr(&method_call.receiver()?)?,
+ name: method_call.name_ref()?,
+ })),
+ }
+ }
+
+ fn for_regular_path(
+ sema: &Semantics<RootDatabase>,
+ path_under_caret: &ast::Path,
+ ) -> Option<Self> {
+ if sema.resolve_path(path_under_caret).is_some() {
+ return None;
+ }
+
+ let segment = path_under_caret.segment()?;
+ let candidate = if let Some(qualifier) = path_under_caret.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 {
+ ty: assoc_item_path.ty(sema.db),
+ name: segment.name_ref()?,
+ })
+ }
+ _ => return None,
+ }
+ } else {
+ ImportCandidate::Path(PathImportCandidate {
+ qualifier: Some(qualifier),
+ name: qualifier_start,
+ })
+ }
+ } else {
+ ImportCandidate::Path(PathImportCandidate {
+ qualifier: None,
+ name: segment.syntax().descendants().find_map(ast::NameRef::cast)?,
+ })
+ };
+ Some(candidate)
+ }
+}
};
use test_utils::mark;
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct InsertUseConfig {
+ pub merge: Option<MergeBehavior>,
+ pub prefix_kind: hir::PrefixKind,
+}
+
#[derive(Debug, Clone)]
pub enum ImportScope {
File(ast::SourceFile),
use either::Either;
use rustc_hash::FxHashSet;
+const QUERY_SEARCH_LIMIT: usize = 40;
+
pub fn find_exact_imports<'a>(
sema: &Semantics<'a, RootDatabase>,
krate: Crate,
{
let mut local_query = symbol_index::Query::new(name_to_import.clone());
local_query.exact();
- local_query.limit(40);
+ local_query.limit(QUERY_SEARCH_LIMIT);
local_query
},
import_map::Query::new(name_to_import)
- .limit(40)
+ .limit(QUERY_SEARCH_LIMIT)
.name_only()
.search_mode(import_map::SearchMode::Equals)
.case_sensitive(),
use std::{env, path::PathBuf, str::FromStr, sync::Arc, time::Instant};
use anyhow::{bail, format_err, Result};
+use hir::PrefixKind;
use ide::{
Analysis, AnalysisHost, Change, CompletionConfig, DiagnosticsConfig, FilePosition, LineCol,
};
salsa::{Database, Durability},
FileId,
},
- helpers::SnippetCap,
+ helpers::{insert_use::InsertUseConfig, SnippetCap},
};
use vfs::AbsPathBuf;
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: SnippetCap::new(true),
- merge: None,
+ insert_use: InsertUseConfig { merge: None, prefix_kind: PrefixKind::Plain },
};
let res = do_work(&mut host, file_id, |analysis| {
analysis.completions(&options, file_position)
use flycheck::FlycheckConfig;
use hir::PrefixKind;
-use ide::{
- AssistConfig, CompletionConfig, DiagnosticsConfig, HoverConfig, InlayHintsConfig,
- InsertUseConfig,
+use ide::{AssistConfig, CompletionConfig, DiagnosticsConfig, HoverConfig, InlayHintsConfig};
+use ide_db::helpers::{
+ insert_use::{InsertUseConfig, MergeBehavior},
+ SnippetCap,
};
-use ide_db::helpers::{insert_use::MergeBehavior, SnippetCap};
use itertools::Itertools;
use lsp_types::{ClientCapabilities, MarkupKind};
use project_model::{CargoConfig, ProjectJson, ProjectJsonData, ProjectManifest};
max_length: self.data.inlayHints_maxLength,
}
}
- fn merge_behavior(&self) -> Option<MergeBehavior> {
- match self.data.assist_importMergeBehavior {
- MergeBehaviorDef::None => None,
- MergeBehaviorDef::Full => Some(MergeBehavior::Full),
- MergeBehaviorDef::Last => Some(MergeBehavior::Last),
+ fn insert_use_config(&self) -> InsertUseConfig {
+ InsertUseConfig {
+ merge: match self.data.assist_importMergeBehavior {
+ MergeBehaviorDef::None => None,
+ MergeBehaviorDef::Full => Some(MergeBehavior::Full),
+ MergeBehaviorDef::Last => Some(MergeBehavior::Last),
+ },
+ prefix_kind: match self.data.assist_importPrefix {
+ ImportPrefixDef::Plain => PrefixKind::Plain,
+ ImportPrefixDef::ByCrate => PrefixKind::ByCrate,
+ ImportPrefixDef::BySelf => PrefixKind::BySelf,
+ },
}
}
pub fn completion(&self) -> CompletionConfig {
&& completion_item_edit_resolve(&self.caps),
add_call_parenthesis: self.data.completion_addCallParenthesis,
add_call_argument_snippets: self.data.completion_addCallArgumentSnippets,
- merge: self.merge_behavior(),
+ insert_use: self.insert_use_config(),
snippet_cap: SnippetCap::new(try_or!(
self.caps
.text_document
snippet_cap: SnippetCap::new(self.experimental("snippetTextEdit")),
allowed: None,
insert_use: InsertUseConfig {
- merge: self.merge_behavior(),
+ merge: match self.data.assist_importMergeBehavior {
+ MergeBehaviorDef::None => None,
+ MergeBehaviorDef::Full => Some(MergeBehavior::Full),
+ MergeBehaviorDef::Last => Some(MergeBehavior::Last),
+ },
prefix_kind: match self.data.assist_importPrefix {
ImportPrefixDef::Plain => PrefixKind::Plain,
ImportPrefixDef::ByCrate => PrefixKind::ByCrate,
#[cfg(test)]
mod tests {
+ use hir::PrefixKind;
use ide::Analysis;
- use ide_db::helpers::SnippetCap;
+ use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap};
use super::*;
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: SnippetCap::new(true),
- merge: None,
+ insert_use: InsertUseConfig { merge: None, prefix_kind: PrefixKind::Plain },
},
ide_db::base_db::FilePosition { file_id, offset },
)