From f62e8616c879255e70052ae35ce7f98bffedac11 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Sep 2020 23:40:11 +0300 Subject: Add imports in auto completion --- crates/assists/src/handlers/auto_import.rs | 3 +- .../handlers/extract_struct_from_enum_variant.rs | 3 +- .../handlers/replace_qualified_name_with_use.rs | 2 +- crates/assists/src/utils.rs | 3 +- crates/assists/src/utils/insert_use.rs | 14 +-- crates/completion/Cargo.toml | 2 + crates/completion/src/completions.rs | 1 + .../completion/src/completions/complete_magic.rs | 114 +++++++++++++++++++++ crates/completion/src/item.rs | 1 + crates/completion/src/lib.rs | 1 + crates/hir/src/code_model.rs | 10 +- crates/hir/src/lib.rs | 1 + crates/ide_db/src/imports_locator.rs | 14 ++- 13 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 crates/completion/src/completions/complete_magic.rs (limited to 'crates') diff --git a/crates/assists/src/handlers/auto_import.rs b/crates/assists/src/handlers/auto_import.rs index 37dd61266..d665837a2 100644 --- a/crates/assists/src/handlers/auto_import.rs +++ b/crates/assists/src/handlers/auto_import.rs @@ -98,7 +98,8 @@ pub(crate) fn auto_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()> let range = ctx.sema.original_range(import_assets.syntax_under_caret()).range; let group = import_group_message(import_assets.import_candidate()); - let scope = ImportScope::find_insert_use_container(import_assets.syntax_under_caret(), ctx)?; + let scope = + ImportScope::find_insert_use_container(import_assets.syntax_under_caret(), &ctx.sema)?; for (import, _) in proposed_imports { acc.add_group( &group, diff --git a/crates/assists/src/handlers/extract_struct_from_enum_variant.rs b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs index 067afabf2..cac77c49b 100644 --- a/crates/assists/src/handlers/extract_struct_from_enum_variant.rs +++ b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs @@ -143,8 +143,7 @@ fn insert_import( if let Some(mut mod_path) = mod_path { mod_path.segments.pop(); mod_path.segments.push(variant_hir_name.clone()); - let scope = ImportScope::find_insert_use_container(scope_node, ctx)?; - + let scope = ImportScope::find_insert_use_container(scope_node, &ctx.sema)?; *rewriter += insert_use(&scope, mod_path_to_ast(&mod_path), ctx.config.insert_use.merge); } Some(()) diff --git a/crates/assists/src/handlers/replace_qualified_name_with_use.rs b/crates/assists/src/handlers/replace_qualified_name_with_use.rs index d7e1d9580..a66db9ae3 100644 --- a/crates/assists/src/handlers/replace_qualified_name_with_use.rs +++ b/crates/assists/src/handlers/replace_qualified_name_with_use.rs @@ -34,7 +34,7 @@ pub(crate) fn replace_qualified_name_with_use( } let target = path.syntax().text_range(); - let scope = ImportScope::find_insert_use_container(path.syntax(), ctx)?; + let scope = ImportScope::find_insert_use_container(path.syntax(), &ctx.sema)?; let syntax = scope.as_syntax_node(); acc.add( AssistId("replace_qualified_name_with_use", AssistKind::RefactorRewrite), diff --git a/crates/assists/src/utils.rs b/crates/assists/src/utils.rs index 7bd338e99..caabc44de 100644 --- a/crates/assists/src/utils.rs +++ b/crates/assists/src/utils.rs @@ -21,8 +21,7 @@ use crate::{ ast_transform::{self, AstTransform, QualifyPaths, SubstituteTypeParams}, }; -pub use insert_use::MergeBehaviour; -pub(crate) use insert_use::{insert_use, ImportScope}; +pub use insert_use::{insert_use, ImportScope, MergeBehaviour}; pub fn mod_path_to_ast(path: &hir::ModPath) -> ast::Path { let mut segments = Vec::new(); diff --git a/crates/assists/src/utils/insert_use.rs b/crates/assists/src/utils/insert_use.rs index af3fc96b6..1aa727e11 100644 --- a/crates/assists/src/utils/insert_use.rs +++ b/crates/assists/src/utils/insert_use.rs @@ -1,6 +1,8 @@ //! Handle syntactic aspects of inserting a new `use`. use std::{cmp::Ordering, iter::successors}; +use hir::Semantics; +use ide_db::RootDatabase; use itertools::{EitherOrBoth, Itertools}; use syntax::{ algo::SyntaxRewriter, @@ -14,7 +16,7 @@ use syntax::{ use test_utils::mark; #[derive(Debug)] -pub(crate) enum ImportScope { +pub enum ImportScope { File(ast::SourceFile), Module(ast::ItemList), } @@ -31,14 +33,14 @@ impl ImportScope { } /// Determines the containing syntax node in which to insert a `use` statement affecting `position`. - pub(crate) fn find_insert_use_container( + pub fn find_insert_use_container( position: &SyntaxNode, - ctx: &crate::assist_context::AssistContext, + sema: &Semantics<'_, RootDatabase>, ) -> Option { - ctx.sema.ancestors_with_macros(position.clone()).find_map(Self::from) + sema.ancestors_with_macros(position.clone()).find_map(Self::from) } - pub(crate) fn as_syntax_node(&self) -> &SyntaxNode { + pub fn as_syntax_node(&self) -> &SyntaxNode { match self { ImportScope::File(file) => file.syntax(), ImportScope::Module(item_list) => item_list.syntax(), @@ -88,7 +90,7 @@ fn is_inner_comment(token: SyntaxToken) -> bool { } /// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur. -pub(crate) fn insert_use<'a>( +pub fn insert_use<'a>( scope: &ImportScope, path: ast::Path, merge: Option, diff --git a/crates/completion/Cargo.toml b/crates/completion/Cargo.toml index 3015ec9e0..799b4a3d5 100644 --- a/crates/completion/Cargo.toml +++ b/crates/completion/Cargo.toml @@ -13,6 +13,7 @@ doctest = false itertools = "0.9.0" log = "0.4.8" rustc-hash = "1.1.0" +either = "1.6.1" assists = { path = "../assists", version = "0.0.0" } stdx = { path = "../stdx", version = "0.0.0" } @@ -21,6 +22,7 @@ text_edit = { path = "../text_edit", version = "0.0.0" } base_db = { path = "../base_db", version = "0.0.0" } ide_db = { path = "../ide_db", version = "0.0.0" } profile = { path = "../profile", version = "0.0.0" } +assists = { path = "../assists", version = "0.0.0" } test_utils = { path = "../test_utils", version = "0.0.0" } # completions crate should depend only on the top-level `hir` package. if you need diff --git a/crates/completion/src/completions.rs b/crates/completion/src/completions.rs index 75dbb1a23..99db5f998 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 complete_magic; use hir::{ModPath, ScopeDef, Type}; diff --git a/crates/completion/src/completions/complete_magic.rs b/crates/completion/src/completions/complete_magic.rs new file mode 100644 index 000000000..857a0b620 --- /dev/null +++ b/crates/completion/src/completions/complete_magic.rs @@ -0,0 +1,114 @@ +//! TODO kb move this into the complete_unqualified_path when starts to work properly + +use assists::utils::{insert_use, mod_path_to_ast, ImportScope, MergeBehaviour}; +use hir::Query; +use itertools::Itertools; +use syntax::AstNode; +use text_edit::TextEdit; + +use crate::{context::CompletionContext, item::CompletionKind, CompletionItem, CompletionItemKind}; + +use super::Completions; + +pub(crate) fn complete_magic(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> { + if !(ctx.is_trivial_path || ctx.is_pat_binding_or_const) { + 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)?; + // TODO kb now this is the whole file, which is not disjoint with any other change in the same file, fix it + // otherwise it's impossible to correctly add the use statement and also change the completed text into something more meaningful + let import_syntax = import_scope.as_syntax_node(); + + // TODO kb consider heuristics, such as "don't show `hash_map` import if `HashMap` is the import for completion" + // TODO kb module functions are not completed, consider `std::io::stdin` one + let potential_import_name = ctx.token.to_string(); + + let possible_imports = ctx + .krate? + // TODO kb use imports_locator instead? + .query_external_importables(ctx.db, Query::new(&potential_import_name).limit(40)) + .unique() + .filter_map(|import_candidate| match import_candidate { + either::Either::Left(module_def) => current_module.find_use_path(ctx.db, module_def), + either::Either::Right(macro_def) => current_module.find_use_path(ctx.db, macro_def), + }) + .filter_map(|mod_path| { + let correct_qualifier = mod_path.segments.last()?.to_string(); + let rewriter = + insert_use(&import_scope, mod_path_to_ast(&mod_path), Some(MergeBehaviour::Full)); + let rewritten_node = rewriter.rewrite(import_syntax); + let insert_use_edit = + TextEdit::replace(import_syntax.text_range(), rewritten_node.to_string()); + let mut completion_edit = + TextEdit::replace(anchor.syntax().text_range(), correct_qualifier); + completion_edit.union(insert_use_edit).expect("TODO kb"); + + let completion_item: CompletionItem = CompletionItem::new( + CompletionKind::Magic, + ctx.source_range(), + mod_path.to_string(), + ) + .kind(CompletionItemKind::Struct) + .text_edit(completion_edit) + .into(); + Some(completion_item) + }); + acc.add_all(possible_imports); + + Some(()) +} + +#[cfg(test)] +mod tests { + use expect_test::{expect, Expect}; + + 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 case_insensitive_magic_completion_works() { + check( + r#" +//- /lib.rs crate:dep +pub struct TestStruct; + +//- /main.rs crate:main deps:dep +fn main() { + teru<|> +} +"#, + expect![[r#" + st dep::TestStruct + "#]], + ); + + check_edit( + "dep::TestStruct", + r#" +//- /lib.rs crate:dep +pub struct TestStruct; + +//- /main.rs crate:main deps:dep +fn main() { + teru<|> +} +"#, + r#" +use dep::TestStruct; + +fn main() { + TestStruct +} +"#, + ); + } +} diff --git a/crates/completion/src/item.rs b/crates/completion/src/item.rs index 6d1d085f4..f23913935 100644 --- a/crates/completion/src/item.rs +++ b/crates/completion/src/item.rs @@ -31,6 +31,7 @@ pub struct CompletionItem { /// /// Typically, replaces `source_range` with new identifier. text_edit: TextEdit, + insert_text_format: InsertTextFormat, /// What item (struct, function, etc) are we completing. diff --git a/crates/completion/src/lib.rs b/crates/completion/src/lib.rs index cb6e0554e..e920fa6b5 100644 --- a/crates/completion/src/lib.rs +++ b/crates/completion/src/lib.rs @@ -118,6 +118,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::complete_magic::complete_magic(&mut acc, &ctx); Some(acc) } diff --git a/crates/hir/src/code_model.rs b/crates/hir/src/code_model.rs index 30a5e4580..37ed092ad 100644 --- a/crates/hir/src/code_model.rs +++ b/crates/hir/src/code_model.rs @@ -110,15 +110,9 @@ impl Crate { pub fn query_external_importables( self, db: &dyn DefDatabase, - query: &str, + query: import_map::Query, ) -> impl Iterator> { - import_map::search_dependencies( - db, - self.into(), - import_map::Query::new(query).anchor_end().case_sensitive().limit(40), - ) - .into_iter() - .map(|item| match item { + import_map::search_dependencies(db, self.into(), query).into_iter().map(|item| match item { ItemInNs::Types(mod_id) | ItemInNs::Values(mod_id) => Either::Left(mod_id.into()), ItemInNs::Macros(mac_id) => Either::Right(mac_id.into()), }) diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 0d184379f..ad58a7cfe 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -49,6 +49,7 @@ pub use hir_def::{ builtin_type::BuiltinType, docs::Documentation, find_path::PrefixKind, + import_map::Query, item_scope::ItemInNs, nameres::ModuleSource, path::{ModPath, PathKind}, diff --git a/crates/ide_db/src/imports_locator.rs b/crates/ide_db/src/imports_locator.rs index df74be00b..e4f4b5427 100644 --- a/crates/ide_db/src/imports_locator.rs +++ b/crates/ide_db/src/imports_locator.rs @@ -1,12 +1,12 @@ //! This module contains an import search funcionality that is provided to the assists module. //! Later, this should be moved away to a separate crate that is accessible from the assists module. -use hir::{Crate, MacroDef, ModuleDef, Semantics}; +use hir::{Crate, MacroDef, ModuleDef, Query as ImportMapQuery, Semantics}; use syntax::{ast, AstNode, SyntaxKind::NAME}; use crate::{ defs::{Definition, NameClass}, - symbol_index::{self, FileSymbol, Query}, + symbol_index::{self, FileSymbol, Query as SymbolQuery}, RootDatabase, }; use either::Either; @@ -21,12 +21,16 @@ pub fn find_imports<'a>( let db = sema.db; // Query dependencies first. - let mut candidates: FxHashSet<_> = - krate.query_external_importables(db, name_to_import).collect(); + let mut candidates: FxHashSet<_> = krate + .query_external_importables( + db, + ImportMapQuery::new(name_to_import).anchor_end().case_sensitive().limit(40), + ) + .collect(); // Query the local crate using the symbol index. let local_results = { - let mut query = Query::new(name_to_import.to_string()); + let mut query = SymbolQuery::new(name_to_import.to_string()); query.exact(); query.limit(40); symbol_index::crate_symbols(db, krate.into(), query) -- cgit v1.2.3