From 6742f38e49d001359a7a9911becc0fcae4c67910 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 16 Jan 2021 19:33:36 +0200 Subject: Share import_assets and related entities --- crates/completion/src/completions.rs | 1 + crates/completion/src/completions/flyimport.rs | 291 +++++++++++++++++++++ .../completion/src/completions/unqualified_path.rs | 251 +----------------- crates/completion/src/config.rs | 4 +- crates/completion/src/lib.rs | 5 +- crates/completion/src/test_utils.rs | 14 +- 6 files changed, 311 insertions(+), 255 deletions(-) create mode 100644 crates/completion/src/completions/flyimport.rs (limited to 'crates/completion') diff --git a/crates/completion/src/completions.rs b/crates/completion/src/completions.rs index 00c9e76f0..c3ce6e51d 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 000000000..54f8e6d24 --- /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 53e1391f3..ac5596ca4 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 b4439b7d1..58fc700f3 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 6cba88a6b..ee1b822e7 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/test_utils.rs b/crates/completion/src/test_utils.rs index 3f4b9d4ac..6ea6da989 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", -- cgit v1.2.3 From 7ae1309ac58bba9e5694629c21287f29f137d6b6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 16 Jan 2021 19:40:59 +0200 Subject: Use specific kind for the flyimport completions --- crates/completion/src/render.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'crates/completion') diff --git a/crates/completion/src/render.rs b/crates/completion/src/render.rs index e93c59f71..820dd01d1 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. -- cgit v1.2.3 From 497fc232e7d90d8d39c7a13742dd85d758dc2f72 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 16 Jan 2021 19:51:42 +0200 Subject: Unindent the test --- crates/completion/src/completions/flyimport.rs | 44 +++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) (limited to 'crates/completion') diff --git a/crates/completion/src/completions/flyimport.rs b/crates/completion/src/completions/flyimport.rs index 54f8e6d24..222809638 100644 --- a/crates/completion/src/completions/flyimport.rs +++ b/crates/completion/src/completions/flyimport.rs @@ -262,29 +262,29 @@ fn main() { 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) {} - } - } +//- /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 - } - "#, +//- /main.rs crate:main deps:dep +use dep::test_mod::TestStruct; +fn main() { + TestSt$0 +} +"#, expect![[r#""#]], ); } -- cgit v1.2.3