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/flyimport.rs | 291 +++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 crates/completion/src/completions/flyimport.rs (limited to 'crates/completion/src/completions/flyimport.rs') 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#""#]], + ); + } +} -- cgit v1.2.3