diff --git a/crates/assists/src/assist_config.rs b/crates/assists/src/assist_config.rs index 4fe8ea7614..9cabf037c0 100644 --- a/crates/assists/src/assist_config.rs +++ b/crates/assists/src/assist_config.rs @@ -4,7 +4,7 @@ //! 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; @@ -14,9 +14,3 @@ pub struct AssistConfig { pub allowed: Option>, pub insert_use: InsertUseConfig, } - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct InsertUseConfig { - pub merge: Option, - pub prefix_kind: hir::PrefixKind, -} diff --git a/crates/assists/src/handlers/auto_import.rs b/crates/assists/src/handlers/auto_import.rs index 55620f0f39..4e2a4fcd96 100644 --- a/crates/assists/src/handlers/auto_import.rs +++ b/crates/assists/src/handlers/auto_import.rs @@ -1,13 +1,11 @@ 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 // @@ -121,8 +119,7 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()> 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) } diff --git a/crates/assists/src/handlers/qualify_path.rs b/crates/assists/src/handlers/qualify_path.rs index f7fbf37f4d..a7d9fd4dc2 100644 --- a/crates/assists/src/handlers/qualify_path.rs +++ b/crates/assists/src/handlers/qualify_path.rs @@ -1,7 +1,10 @@ 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, @@ -12,7 +15,6 @@ use test_utils::mark; use crate::{ assist_context::{AssistContext, Assists}, - utils::import_assets::{ImportAssets, ImportCandidate}, AssistId, AssistKind, GroupLabel, }; @@ -53,17 +55,18 @@ pub(crate) fn qualify_path(acc: &mut Assists, ctx: &AssistContext) -> Option<()> 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); @@ -186,7 +189,7 @@ fn item_as_trait(item: hir::ItemInNs) -> Option { 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)) @@ -194,8 +197,13 @@ fn group_label(candidate: &ImportCandidate) -> GroupLabel { 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), } diff --git a/crates/assists/src/lib.rs b/crates/assists/src/lib.rs index 3d79718060..14178a6510 100644 --- a/crates/assists/src/lib.rs +++ b/crates/assists/src/lib.rs @@ -24,7 +24,7 @@ use syntax::TextRange; 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 { diff --git a/crates/assists/src/tests.rs b/crates/assists/src/tests.rs index 71431b4065..32bd8698b1 100644 --- a/crates/assists/src/tests.rs +++ b/crates/assists/src/tests.rs @@ -3,16 +3,17 @@ mod generated; 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 { diff --git a/crates/assists/src/utils.rs b/crates/assists/src/utils.rs index 9ea96eb73b..fc9f83bab4 100644 --- a/crates/assists/src/utils.rs +++ b/crates/assists/src/utils.rs @@ -1,5 +1,4 @@ //! Assorted functions shared by several assists. -pub(crate) mod import_assets; use std::ops; diff --git a/crates/completion/src/completions.rs b/crates/completion/src/completions.rs index 00c9e76f03..c3ce6e51d5 100644 --- a/crates/completion/src/completions.rs +++ b/crates/completion/src/completions.rs @@ -13,6 +13,7 @@ pub(crate) mod postfix; 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}; diff --git a/crates/completion/src/completions/flyimport.rs b/crates/completion/src/completions/flyimport.rs new file mode 100644 index 0000000000..2228096384 --- /dev/null +++ b/crates/completion/src/completions/flyimport.rs @@ -0,0 +1,291 @@ +//! 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::>(); + + 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#""#]], + ); + } +} diff --git a/crates/completion/src/completions/unqualified_path.rs b/crates/completion/src/completions/unqualified_path.rs index 53e1391f34..ac5596ca49 100644 --- a/crates/completion/src/completions/unqualified_path.rs +++ b/crates/completion/src/completions/unqualified_path.rs @@ -2,17 +2,11 @@ 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) { @@ -45,10 +39,6 @@ pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionC } 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) { @@ -77,124 +67,13 @@ fn complete_enum_variants(acc: &mut Completions, ctx: &CompletionContext, ty: &T } } -// 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::>(); - - 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, }; @@ -855,128 +734,4 @@ impl My$0 "#]], ) } - - #[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 - "#]], - ); - } } diff --git a/crates/completion/src/config.rs b/crates/completion/src/config.rs index b4439b7d14..58fc700f33 100644 --- a/crates/completion/src/config.rs +++ b/crates/completion/src/config.rs @@ -4,7 +4,7 @@ //! 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 { @@ -13,5 +13,5 @@ pub struct CompletionConfig { pub add_call_parenthesis: bool, pub add_call_argument_snippets: bool, pub snippet_cap: Option, - pub merge: Option, + pub insert_use: InsertUseConfig, } diff --git a/crates/completion/src/lib.rs b/crates/completion/src/lib.rs index 6cba88a6b8..ee1b822e7c 100644 --- a/crates/completion/src/lib.rs +++ b/crates/completion/src/lib.rs @@ -127,6 +127,7 @@ pub fn completions( 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) } @@ -153,7 +154,9 @@ pub fn resolve_completion_edits( }) .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)] diff --git a/crates/completion/src/render.rs b/crates/completion/src/render.rs index e93c59f717..820dd01d1f 100644 --- a/crates/completion/src/render.rs +++ b/crates/completion/src/render.rs @@ -51,11 +51,16 @@ pub(crate) fn render_resolution_with_import<'a>( import_edit: ImportEdit, resolution: &ScopeDef, ) -> Option { - Render::new(ctx).render_resolution( - import_edit.import_path.segments.last()?.to_string(), - Some(import_edit), - resolution, - ) + Render::new(ctx) + .render_resolution( + import_edit.import_path.segments.last()?.to_string(), + Some(import_edit), + resolution, + ) + .map(|mut item| { + item.completion_kind = CompletionKind::Magic; + item + }) } /// Interface for data and methods required for items rendering. diff --git a/crates/completion/src/test_utils.rs b/crates/completion/src/test_utils.rs index 3f4b9d4acc..6ea6da9893 100644 --- a/crates/completion/src/test_utils.rs +++ b/crates/completion/src/test_utils.rs @@ -1,9 +1,12 @@ //! 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; @@ -19,7 +22,10 @@ pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig { 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. @@ -110,7 +116,7 @@ pub(crate) fn check_edit_with_config( 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", diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index afd552008d..f8d69382e0 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -80,7 +80,7 @@ pub use crate::{ 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, diff --git a/crates/ide_db/src/helpers.rs b/crates/ide_db/src/helpers.rs index c6763ae369..0dcc4dd29d 100644 --- a/crates/ide_db/src/helpers.rs +++ b/crates/ide_db/src/helpers.rs @@ -1,5 +1,6 @@ //! 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}; diff --git a/crates/assists/src/utils/import_assets.rs b/crates/ide_db/src/helpers/import_assets.rs similarity index 88% rename from crates/assists/src/utils/import_assets.rs rename to crates/ide_db/src/helpers/import_assets.rs index 4ce82c1ba5..edc3da318e 100644 --- a/crates/assists/src/utils/import_assets.rs +++ b/crates/ide_db/src/helpers/import_assets.rs @@ -1,19 +1,17 @@ //! 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; +use crate::{imports_locator, RootDatabase}; + +use super::insert_use::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), +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` @@ -25,25 +23,26 @@ pub(crate) enum ImportCandidate { } #[derive(Debug)] -pub(crate) struct TraitImportCandidate { - pub(crate) ty: hir::Type, - pub(crate) name: ast::NameRef, +pub struct TraitImportCandidate { + pub ty: hir::Type, + pub name: ast::NameRef, } #[derive(Debug)] -pub(crate) struct PathImportCandidate { - pub(crate) name: ast::NameRef, +pub struct PathImportCandidate { + pub qualifier: Option, + pub name: ast::NameRef, } #[derive(Debug)] -pub(crate) struct ImportAssets { +pub struct ImportAssets { import_candidate: ImportCandidate, module_with_name_to_import: hir::Module, syntax_under_caret: SyntaxNode, } impl ImportAssets { - pub(crate) fn for_method_call( + pub fn for_method_call( method_call: ast::MethodCallExpr, sema: &Semantics, ) -> Option { @@ -56,7 +55,7 @@ impl ImportAssets { }) } - pub(crate) fn for_regular_path( + pub fn for_regular_path( path_under_caret: ast::Path, sema: &Semantics, ) -> Option { @@ -73,24 +72,23 @@ impl ImportAssets { }) } - pub(crate) fn syntax_under_caret(&self) -> &SyntaxNode { + pub fn syntax_under_caret(&self) -> &SyntaxNode { &self.syntax_under_caret } - pub(crate) fn import_candidate(&self) -> &ImportCandidate { + pub 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::Path(candidate) => candidate.name.text(), ImportCandidate::TraitAssocItem(candidate) | ImportCandidate::TraitMethod(candidate) => candidate.name.text(), } } - pub(crate) fn search_for_imports( + pub fn search_for_imports( &self, sema: &Semantics, config: &InsertUseConfig, @@ -101,7 +99,7 @@ impl ImportAssets { /// 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( + pub fn search_for_relative_paths( &self, sema: &Semantics, ) -> Vec<(hir::ModPath, hir::ItemInNs)> { @@ -253,10 +251,14 @@ impl ImportCandidate { _ => return None, } } else { - ImportCandidate::QualifierStart(PathImportCandidate { name: qualifier_start }) + ImportCandidate::Path(PathImportCandidate { + qualifier: Some(qualifier), + name: qualifier_start, + }) } } else { - ImportCandidate::UnqualifiedName(PathImportCandidate { + ImportCandidate::Path(PathImportCandidate { + qualifier: None, name: segment.syntax().descendants().find_map(ast::NameRef::cast)?, }) }; diff --git a/crates/ide_db/src/helpers/insert_use.rs b/crates/ide_db/src/helpers/insert_use.rs index d2f9f5d25c..877d4f1c79 100644 --- a/crates/ide_db/src/helpers/insert_use.rs +++ b/crates/ide_db/src/helpers/insert_use.rs @@ -15,6 +15,12 @@ use syntax::{ }; use test_utils::mark; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct InsertUseConfig { + pub merge: Option, + pub prefix_kind: hir::PrefixKind, +} + #[derive(Debug, Clone)] pub enum ImportScope { File(ast::SourceFile), diff --git a/crates/ide_db/src/imports_locator.rs b/crates/ide_db/src/imports_locator.rs index e9f23adf83..d111fba925 100644 --- a/crates/ide_db/src/imports_locator.rs +++ b/crates/ide_db/src/imports_locator.rs @@ -12,6 +12,8 @@ use crate::{ 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, @@ -24,11 +26,11 @@ pub fn find_exact_imports<'a>( { 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(), diff --git a/crates/rust-analyzer/src/cli/analysis_bench.rs b/crates/rust-analyzer/src/cli/analysis_bench.rs index 7d3fda7a84..a02c8327fe 100644 --- a/crates/rust-analyzer/src/cli/analysis_bench.rs +++ b/crates/rust-analyzer/src/cli/analysis_bench.rs @@ -3,6 +3,7 @@ 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, }; @@ -11,7 +12,7 @@ use ide_db::{ salsa::{Database, Durability}, FileId, }, - helpers::SnippetCap, + helpers::{insert_use::InsertUseConfig, SnippetCap}, }; use vfs::AbsPathBuf; @@ -96,7 +97,7 @@ impl BenchCmd { 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) diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 27b92a5a9b..ce9655818c 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -11,11 +11,11 @@ use std::{convert::TryFrom, ffi::OsString, iter, path::PathBuf}; 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}; @@ -542,11 +542,18 @@ impl Config { max_length: self.data.inlayHints_maxLength, } } - fn merge_behavior(&self) -> Option { - 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 { @@ -556,7 +563,7 @@ impl Config { && 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 @@ -575,7 +582,11 @@ impl Config { 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, diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index dc67d19a75..1ff2d3fea6 100644 --- a/crates/rust-analyzer/src/to_proto.rs +++ b/crates/rust-analyzer/src/to_proto.rs @@ -861,8 +861,9 @@ pub(crate) fn rename_error(err: RenameError) -> crate::LspError { #[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::*; @@ -887,7 +888,7 @@ mod tests { 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 }, )