]> git.lizzy.rs Git - rust.git/commitdiff
Merge #6706
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>
Tue, 8 Dec 2020 13:10:28 +0000 (13:10 +0000)
committerGitHub <noreply@github.com>
Tue, 8 Dec 2020 13:10:28 +0000 (13:10 +0000)
6706: Move import text edit calculation into a completion resolve request r=matklad a=SomeoneToIgnore

Part of https://github.com/rust-analyzer/rust-analyzer/issues/6612 (presumably fixing it)
Part of https://github.com/rust-analyzer/rust-analyzer/issues/6366 (does not cover all possible resolve capabilities we can do)
Closes https://github.com/rust-analyzer/rust-analyzer/issues/6594

Further improves imports on completion performance by deferring the computations for import inserts.

To use the new mode, you have to have the experimental completions enabled and use the LSP 3.16-compliant client that reports `additionalTextEdits` in its `CompletionItemCapabilityResolveSupport` field in the client capabilities.
rust-analyzer VSCode extension does this already hence picks up the changes completely.

Performance implications are descrbed in: https://github.com/rust-analyzer/rust-analyzer/issues/6633#issuecomment-737295182

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
17 files changed:
crates/completion/src/completions/unqualified_path.rs
crates/completion/src/config.rs
crates/completion/src/item.rs
crates/completion/src/lib.rs
crates/completion/src/render.rs
crates/completion/src/render/enum_variant.rs
crates/completion/src/render/function.rs
crates/completion/src/render/macro_.rs
crates/completion/src/test_utils.rs
crates/ide/src/lib.rs
crates/ide_db/src/imports_locator.rs
crates/rust-analyzer/src/caps.rs
crates/rust-analyzer/src/config.rs
crates/rust-analyzer/src/handlers.rs
crates/rust-analyzer/src/lsp_utils.rs
crates/rust-analyzer/src/main_loop.rs
editors/code/package.json

index 81691cd7f23bc2d6b2911e26d85478b5302add17..4e4e2b36f07c75a3e56f6351e45e05295489c690 100644 (file)
@@ -9,7 +9,7 @@
 
 use crate::{
     render::{render_resolution_with_import, RenderContext},
-    CompletionContext, Completions,
+    CompletionContext, Completions, ImportEdit,
 };
 
 pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionContext) {
@@ -44,7 +44,7 @@ pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionC
         acc.add_resolution(ctx, name.to_string(), &res)
     });
 
-    if ctx.config.enable_experimental_completions {
+    if ctx.config.enable_autoimport_completions && ctx.config.resolve_additional_edits_lazily() {
         fuzzy_completion(acc, ctx).unwrap_or_default()
     }
 }
@@ -73,19 +73,64 @@ 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<|>
+// }
+// # 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 identifiers only
+// (i.e. in `HashMap` in the `std::collections::HashMap` path), also not in the module indentifiers.
+//
+// .Merge Behaviour
+//
+// It is possible to configure how use-trees are merged with the `importMergeBehaviour` setting.
+// Mimics the corresponding behaviour 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 _p = profile::span("fuzzy_completion");
+    let potential_import_name = ctx.token.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 potential_import_name = ctx.token.to_string();
-
     let possible_imports = imports_locator::find_similar_imports(
         &ctx.sema,
         ctx.krate?,
+        Some(100),
         &potential_import_name,
-        50,
         true,
     )
     .filter_map(|import_candidate| {
@@ -99,13 +144,14 @@ fn fuzzy_completion(acc: &mut Completions, ctx: &CompletionContext) -> Option<()
         })
     })
     .filter(|(mod_path, _)| mod_path.len() > 1)
-    .take(20)
     .filter_map(|(import_path, definition)| {
         render_resolution_with_import(
             RenderContext::new(ctx),
-            import_path.clone(),
-            import_scope.clone(),
-            ctx.config.merge,
+            ImportEdit {
+                import_path: import_path.clone(),
+                import_scope: import_scope.clone(),
+                merge_behaviour: ctx.config.merge,
+            },
             &definition,
         )
     });
@@ -120,8 +166,8 @@ mod tests {
     use test_utils::mark;
 
     use crate::{
-        test_utils::{check_edit, completion_list},
-        CompletionKind,
+        test_utils::{check_edit, check_edit_with_config, completion_list},
+        CompletionConfig, CompletionKind,
     };
 
     fn check(ra_fixture: &str, expect: Expect) {
@@ -730,7 +776,13 @@ impl My<|>
 
     #[test]
     fn function_fuzzy_completion() {
-        check_edit(
+        let mut completion_config = CompletionConfig::default();
+        completion_config
+            .active_resolve_capabilities
+            .insert(crate::CompletionResolveCapability::AdditionalTextEdits);
+
+        check_edit_with_config(
+            completion_config,
             "stdin",
             r#"
 //- /lib.rs crate:dep
@@ -755,7 +807,13 @@ fn main() {
 
     #[test]
     fn macro_fuzzy_completion() {
-        check_edit(
+        let mut completion_config = CompletionConfig::default();
+        completion_config
+            .active_resolve_capabilities
+            .insert(crate::CompletionResolveCapability::AdditionalTextEdits);
+
+        check_edit_with_config(
+            completion_config,
             "macro_with_curlies!",
             r#"
 //- /lib.rs crate:dep
@@ -782,7 +840,13 @@ fn main() {
 
     #[test]
     fn struct_fuzzy_completion() {
-        check_edit(
+        let mut completion_config = CompletionConfig::default();
+        completion_config
+            .active_resolve_capabilities
+            .insert(crate::CompletionResolveCapability::AdditionalTextEdits);
+
+        check_edit_with_config(
+            completion_config,
             "ThirdStruct",
             r#"
 //- /lib.rs crate:dep
index 654a76f7b33e2077247a90e431e250674d1e6225..5175b9d69dc2517a7f585dfd6bb6d0a2245e62c2 100644 (file)
@@ -5,21 +5,42 @@
 //! completions if we are allowed to.
 
 use ide_db::helpers::insert_use::MergeBehaviour;
+use rustc_hash::FxHashSet;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct CompletionConfig {
     pub enable_postfix_completions: bool,
-    pub enable_experimental_completions: bool,
+    pub enable_autoimport_completions: bool,
     pub add_call_parenthesis: bool,
     pub add_call_argument_snippets: bool,
     pub snippet_cap: Option<SnippetCap>,
     pub merge: Option<MergeBehaviour>,
+    /// A set of capabilities, enabled on the client and supported on the server.
+    pub active_resolve_capabilities: FxHashSet<CompletionResolveCapability>,
+}
+
+/// A resolve capability, supported on the server.
+/// If the client registers any completion resolve capabilities,
+/// the server is able to render completion items' corresponding fields later,
+/// not during an initial completion item request.
+/// See https://github.com/rust-analyzer/rust-analyzer/issues/6366 for more details.
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
+pub enum CompletionResolveCapability {
+    Documentation,
+    Detail,
+    AdditionalTextEdits,
 }
 
 impl CompletionConfig {
     pub fn allow_snippets(&mut self, yes: bool) {
         self.snippet_cap = if yes { Some(SnippetCap { _private: () }) } else { None }
     }
+
+    /// Whether the completions' additional edits are calculated when sending an initional completions list
+    /// or later, in a separate resolve request.
+    pub fn resolve_additional_edits_lazily(&self) -> bool {
+        self.active_resolve_capabilities.contains(&CompletionResolveCapability::AdditionalTextEdits)
+    }
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -31,11 +52,12 @@ impl Default for CompletionConfig {
     fn default() -> Self {
         CompletionConfig {
             enable_postfix_completions: true,
-            enable_experimental_completions: true,
+            enable_autoimport_completions: true,
             add_call_parenthesis: true,
             add_call_argument_snippets: true,
             snippet_cap: Some(SnippetCap { _private: () }),
             merge: Some(MergeBehaviour::Full),
+            active_resolve_capabilities: FxHashSet::default(),
         }
     }
 }
index e85549fef4d45370defc2ea58546fb69e55e5a5e..bd94402d75364ed43cf130687c2b742ddf8510eb 100644 (file)
@@ -15,6 +15,7 @@
 /// `CompletionItem` describes a single completion variant in the editor pop-up.
 /// It is basically a POD with various properties. To construct a
 /// `CompletionItem`, use `new` method and the `Builder` struct.
+#[derive(Clone)]
 pub struct CompletionItem {
     /// Used only internally in tests, to check only specific kind of
     /// completion (postfix, keyword, reference, etc).
@@ -65,6 +66,9 @@ pub struct CompletionItem {
     /// Indicates that a reference or mutable reference to this variable is a
     /// possible match.
     ref_match: Option<(Mutability, CompletionScore)>,
+
+    /// The import data to add to completion's edits.
+    import_to_add: Option<ImportEdit>,
 }
 
 // We use custom debug for CompletionItem to make snapshot tests more readable.
@@ -256,14 +260,37 @@ pub fn trigger_call_info(&self) -> bool {
     pub fn ref_match(&self) -> Option<(Mutability, CompletionScore)> {
         self.ref_match
     }
+
+    pub fn import_to_add(&self) -> Option<&ImportEdit> {
+        self.import_to_add.as_ref()
+    }
 }
 
 /// An extra import to add after the completion is applied.
-#[derive(Clone)]
-pub(crate) struct ImportToAdd {
-    pub(crate) import_path: ModPath,
-    pub(crate) import_scope: ImportScope,
-    pub(crate) merge_behaviour: Option<MergeBehaviour>,
+#[derive(Debug, Clone)]
+pub struct ImportEdit {
+    pub import_path: ModPath,
+    pub import_scope: ImportScope,
+    pub merge_behaviour: Option<MergeBehaviour>,
+}
+
+impl ImportEdit {
+    /// Attempts to insert the import to the given scope, producing a text edit.
+    /// May return no edit in edge cases, such as scope already containing the import.
+    pub fn to_text_edit(&self) -> Option<TextEdit> {
+        let _p = profile::span("ImportEdit::to_text_edit");
+
+        let rewriter = insert_use::insert_use(
+            &self.import_scope,
+            mod_path_to_ast(&self.import_path),
+            self.merge_behaviour,
+        );
+        let old_ast = rewriter.rewrite_root()?;
+        let mut import_insert = TextEdit::builder();
+        algo::diff(&old_ast, &rewriter.rewrite(&old_ast)).into_text_edit(&mut import_insert);
+
+        Some(import_insert.finish())
+    }
 }
 
 /// A helper to make `CompletionItem`s.
@@ -272,7 +299,7 @@ pub(crate) struct ImportToAdd {
 pub(crate) struct Builder {
     source_range: TextRange,
     completion_kind: CompletionKind,
-    import_to_add: Option<ImportToAdd>,
+    import_to_add: Option<ImportEdit>,
     label: String,
     insert_text: Option<String>,
     insert_text_format: InsertTextFormat,
@@ -294,11 +321,9 @@ pub(crate) fn build(self) -> CompletionItem {
         let mut label = self.label;
         let mut lookup = self.lookup;
         let mut insert_text = self.insert_text;
-        let mut text_edits = TextEdit::builder();
 
-        if let Some(import_data) = self.import_to_add {
-            let import = mod_path_to_ast(&import_data.import_path);
-            let mut import_path_without_last_segment = import_data.import_path;
+        if let Some(import_to_add) = self.import_to_add.as_ref() {
+            let mut import_path_without_last_segment = import_to_add.import_path.to_owned();
             let _ = import_path_without_last_segment.segments.pop();
 
             if !import_path_without_last_segment.segments.is_empty() {
@@ -310,32 +335,20 @@ pub(crate) fn build(self) -> CompletionItem {
                 }
                 label = format!("{}::{}", import_path_without_last_segment, label);
             }
-
-            let rewriter = insert_use::insert_use(
-                &import_data.import_scope,
-                import,
-                import_data.merge_behaviour,
-            );
-            if let Some(old_ast) = rewriter.rewrite_root() {
-                algo::diff(&old_ast, &rewriter.rewrite(&old_ast)).into_text_edit(&mut text_edits);
-            }
         }
 
-        let original_edit = match self.text_edit {
+        let text_edit = match self.text_edit {
             Some(it) => it,
             None => {
                 TextEdit::replace(self.source_range, insert_text.unwrap_or_else(|| label.clone()))
             }
         };
 
-        let mut resulting_edit = text_edits.finish();
-        resulting_edit.union(original_edit).expect("Failed to unite text edits");
-
         CompletionItem {
             source_range: self.source_range,
             label,
             insert_text_format: self.insert_text_format,
-            text_edit: resulting_edit,
+            text_edit,
             detail: self.detail,
             documentation: self.documentation,
             lookup,
@@ -345,6 +358,7 @@ pub(crate) fn build(self) -> CompletionItem {
             trigger_call_info: self.trigger_call_info.unwrap_or(false),
             score: self.score,
             ref_match: self.ref_match,
+            import_to_add: self.import_to_add,
         }
     }
     pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder {
@@ -407,7 +421,7 @@ pub(crate) fn trigger_call_info(mut self) -> Builder {
         self.trigger_call_info = Some(true);
         self
     }
-    pub(crate) fn add_import(mut self, import_to_add: Option<ImportToAdd>) -> Builder {
+    pub(crate) fn add_import(mut self, import_to_add: Option<ImportEdit>) -> Builder {
         self.import_to_add = import_to_add;
         self
     }
index 1ec2e9be72de5433a46669198c2b95a671d3d229..f60f87243fb03862c4faee8045284addbce4ffaa 100644 (file)
 
 mod completions;
 
-use ide_db::base_db::FilePosition;
-use ide_db::RootDatabase;
+use ide_db::{
+    base_db::FilePosition, helpers::insert_use::ImportScope, imports_locator, RootDatabase,
+};
+use syntax::AstNode;
+use text_edit::TextEdit;
 
 use crate::{completions::Completions, context::CompletionContext, item::CompletionKind};
 
 pub use crate::{
-    config::CompletionConfig,
-    item::{CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat},
+    config::{CompletionConfig, CompletionResolveCapability},
+    item::{CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, InsertTextFormat},
 };
 
 //FIXME: split the following feature into fine-grained features.
 // }
 // ```
 //
-// And experimental completions, enabled with the `rust-analyzer.completion.enableExperimental` setting.
-// This flag enables or disables:
-//
-// - Auto import: additional completion options with automatic `use` import and options from all project importable items, matched for the input
-//
-// Experimental completions might cause issues with performance and completion list look.
+// And the auto import completions, enabled with the `rust-analyzer.completion.autoimport.enable` setting and the corresponding LSP client capabilities.
+// Those are the additional completion options with automatic `use` import and options from all project importable items,
+// fuzzy matched agains the completion imput.
 
 /// Main entry point for completion. We run completion as a two-phase process.
 ///
@@ -131,6 +131,33 @@ pub fn completions(
     Some(acc)
 }
 
+/// Resolves additional completion data at the position given.
+pub fn resolve_completion_edits(
+    db: &RootDatabase,
+    config: &CompletionConfig,
+    position: FilePosition,
+    full_import_path: &str,
+    imported_name: &str,
+) -> Option<Vec<TextEdit>> {
+    let ctx = CompletionContext::new(db, position, config)?;
+    let anchor = ctx.name_ref_syntax.as_ref()?;
+    let import_scope = ImportScope::find_insert_use_container(anchor.syntax(), &ctx.sema)?;
+
+    let current_module = ctx.sema.scope(anchor.syntax()).module()?;
+    let current_crate = current_module.krate();
+
+    let import_path = imports_locator::find_exact_imports(&ctx.sema, current_crate, imported_name)
+        .filter_map(|candidate| {
+            let item: hir::ItemInNs = candidate.either(Into::into, Into::into);
+            current_module.find_use_path(db, item)
+        })
+        .find(|mod_path| mod_path.to_string() == full_import_path)?;
+
+    ImportEdit { import_path, import_scope, merge_behaviour: config.merge }
+        .to_text_edit()
+        .map(|edit| vec![edit])
+}
+
 #[cfg(test)]
 mod tests {
     use crate::config::CompletionConfig;
index 504757a6ae2ff271ee9cb31279a806ac7e8c1de8..b940388df26aabbd40dddbeed4f27092763c7ca4 100644 (file)
@@ -9,14 +9,13 @@
 
 mod builder_ext;
 
-use hir::{Documentation, HasAttrs, HirDisplay, ModPath, Mutability, ScopeDef, Type};
-use ide_db::helpers::insert_use::{ImportScope, MergeBehaviour};
+use hir::{Documentation, HasAttrs, HirDisplay, Mutability, ScopeDef, Type};
 use ide_db::RootDatabase;
 use syntax::TextRange;
 use test_utils::mark;
 
 use crate::{
-    config::SnippetCap, item::ImportToAdd, CompletionContext, CompletionItem, CompletionItemKind,
+    config::SnippetCap, item::ImportEdit, CompletionContext, CompletionItem, CompletionItemKind,
     CompletionKind, CompletionScore,
 };
 
@@ -48,15 +47,12 @@ pub(crate) fn render_resolution<'a>(
 
 pub(crate) fn render_resolution_with_import<'a>(
     ctx: RenderContext<'a>,
-    import_path: ModPath,
-    import_scope: ImportScope,
-    merge_behaviour: Option<MergeBehaviour>,
+    import_edit: ImportEdit,
     resolution: &ScopeDef,
 ) -> Option<CompletionItem> {
-    let local_name = import_path.segments.last()?.to_string();
     Render::new(ctx).render_resolution(
-        local_name,
-        Some(ImportToAdd { import_path, import_scope, merge_behaviour }),
+        import_edit.import_path.segments.last()?.to_string(),
+        Some(import_edit),
         resolution,
     )
 }
@@ -147,7 +143,7 @@ fn add_tuple_field(&mut self, field: usize, ty: &Type) -> CompletionItem {
     fn render_resolution(
         self,
         local_name: String,
-        import_to_add: Option<ImportToAdd>,
+        import_to_add: Option<ImportEdit>,
         resolution: &ScopeDef,
     ) -> Option<CompletionItem> {
         let _p = profile::span("render_resolution");
@@ -450,28 +446,6 @@ fn main() { let _: m::Spam = S<|> }
                         insert: "m",
                         kind: Module,
                     },
-                    CompletionItem {
-                        label: "m::Spam",
-                        source_range: 75..76,
-                        text_edit: TextEdit {
-                            indels: [
-                                Indel {
-                                    insert: "use m::Spam;",
-                                    delete: 0..0,
-                                },
-                                Indel {
-                                    insert: "\n\n",
-                                    delete: 0..0,
-                                },
-                                Indel {
-                                    insert: "Spam",
-                                    delete: 75..76,
-                                },
-                            ],
-                        },
-                        kind: Enum,
-                        lookup: "Spam",
-                    },
                     CompletionItem {
                         label: "m::Spam::Foo",
                         source_range: 75..76,
index f4bd02f258155c7359ab492121bd7e8f6f126b46..8e0fea6c0fc5768e6ee2acfa912371f323c371ea 100644 (file)
@@ -5,13 +5,13 @@
 use test_utils::mark;
 
 use crate::{
-    item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
+    item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
     render::{builder_ext::Params, RenderContext},
 };
 
 pub(crate) fn render_enum_variant<'a>(
     ctx: RenderContext<'a>,
-    import_to_add: Option<ImportToAdd>,
+    import_to_add: Option<ImportEdit>,
     local_name: Option<String>,
     variant: hir::EnumVariant,
     path: Option<ModPath>,
@@ -62,7 +62,7 @@ fn new(
         }
     }
 
-    fn render(self, import_to_add: Option<ImportToAdd>) -> CompletionItem {
+    fn render(self, import_to_add: Option<ImportEdit>) -> CompletionItem {
         let mut builder = CompletionItem::new(
             CompletionKind::Reference,
             self.ctx.source_range(),
index 00e3eb203e3ac4bb5bc3f0055d325d568feef873..d16005249c0cee3b2f22f181fa523cfbb3623303 100644 (file)
@@ -5,13 +5,13 @@
 use test_utils::mark;
 
 use crate::{
-    item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
+    item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
     render::{builder_ext::Params, RenderContext},
 };
 
 pub(crate) fn render_fn<'a>(
     ctx: RenderContext<'a>,
-    import_to_add: Option<ImportToAdd>,
+    import_to_add: Option<ImportEdit>,
     local_name: Option<String>,
     fn_: hir::Function,
 ) -> CompletionItem {
@@ -39,7 +39,7 @@ fn new(
         FunctionRender { ctx, name, func: fn_, ast_node }
     }
 
-    fn render(self, import_to_add: Option<ImportToAdd>) -> CompletionItem {
+    fn render(self, import_to_add: Option<ImportEdit>) -> CompletionItem {
         let params = self.params();
         CompletionItem::new(CompletionKind::Reference, self.ctx.source_range(), self.name.clone())
             .kind(self.kind())
index b4ab32c6e43ce35d8516874f1c2c9945c8173866..eb3209bee3dcf8e6a599110763e048dd255bcaa6 100644 (file)
@@ -5,13 +5,13 @@
 use test_utils::mark;
 
 use crate::{
-    item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
+    item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
     render::RenderContext,
 };
 
 pub(crate) fn render_macro<'a>(
     ctx: RenderContext<'a>,
-    import_to_add: Option<ImportToAdd>,
+    import_to_add: Option<ImportEdit>,
     name: String,
     macro_: hir::MacroDef,
 ) -> Option<CompletionItem> {
@@ -38,7 +38,7 @@ fn new(ctx: RenderContext<'a>, name: String, macro_: hir::MacroDef) -> MacroRend
         MacroRender { ctx, name, macro_, docs, bra, ket }
     }
 
-    fn render(&self, import_to_add: Option<ImportToAdd>) -> Option<CompletionItem> {
+    fn render(&self, import_to_add: Option<ImportEdit>) -> Option<CompletionItem> {
         // FIXME: Currently proc-macro do not have ast-node,
         // such that it does not have source
         if self.macro_.is_proc_macro() {
index 4c1b1a83923ac80d00ce3a431ac2de3d104643e5..25f5f4924cd53ee06768135dad297194ec9994e0 100644 (file)
@@ -96,7 +96,16 @@ pub(crate) fn check_edit_with_config(
         .collect_tuple()
         .unwrap_or_else(|| panic!("can't find {:?} completion in {:#?}", what, completions));
     let mut actual = db.file_text(position.file_id).to_string();
-    completion.text_edit().apply(&mut actual);
+
+    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())
+    {
+        combined_edit.union(import_text_edit).expect(
+            "Failed to apply completion resolve changes: change ranges overlap, but should not",
+        )
+    }
+
+    combined_edit.apply(&mut actual);
     assert_eq_text!(&ra_fixture_after, &actual)
 }
 
index 5244bdd610e086fcc0be5255c2058f43bcbd3932..71068cac28aa9021c25784de64d6bfc8429aa7bd 100644 (file)
@@ -80,7 +80,8 @@ macro_rules! eprintln {
     },
 };
 pub use completion::{
-    CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat,
+    CompletionConfig, CompletionItem, CompletionItemKind, CompletionResolveCapability,
+    CompletionScore, ImportEdit, InsertTextFormat,
 };
 pub use ide_db::{
     call_info::CallInfo,
@@ -468,6 +469,27 @@ pub fn completions(
         self.with_db(|db| completion::completions(db, config, position).map(Into::into))
     }
 
+    /// Resolves additional completion data at the position given.
+    pub fn resolve_completion_edits(
+        &self,
+        config: &CompletionConfig,
+        position: FilePosition,
+        full_import_path: &str,
+        imported_name: &str,
+    ) -> Cancelable<Vec<TextEdit>> {
+        Ok(self
+            .with_db(|db| {
+                completion::resolve_completion_edits(
+                    db,
+                    config,
+                    position,
+                    full_import_path,
+                    imported_name,
+                )
+            })?
+            .unwrap_or_default())
+    }
+
     /// Computes resolved assists with source changes for the given position.
     pub fn resolved_assists(
         &self,
index 09046d3c36a8dad72bfd1cf3bc7d8cad9d8071c4..b2980a5d69df2e651791ddc74c1b92e0d679f062 100644 (file)
@@ -34,27 +34,25 @@ pub fn find_exact_imports<'a>(
 pub fn find_similar_imports<'a>(
     sema: &Semantics<'a, RootDatabase>,
     krate: Crate,
+    limit: Option<usize>,
     name_to_import: &str,
-    limit: usize,
     ignore_modules: bool,
 ) -> impl Iterator<Item = Either<ModuleDef, MacroDef>> {
     let _p = profile::span("find_similar_imports");
 
-    let mut external_query = import_map::Query::new(name_to_import).limit(limit);
+    let mut external_query = import_map::Query::new(name_to_import);
     if ignore_modules {
         external_query = external_query.exclude_import_kind(import_map::ImportKind::Module);
     }
 
-    find_imports(
-        sema,
-        krate,
-        {
-            let mut local_query = symbol_index::Query::new(name_to_import.to_string());
-            local_query.limit(limit);
-            local_query
-        },
-        external_query,
-    )
+    let mut local_query = symbol_index::Query::new(name_to_import.to_string());
+
+    if let Some(limit) = limit {
+        local_query.limit(limit);
+        external_query = external_query.limit(limit);
+    }
+
+    find_imports(sema, krate, local_query, external_query)
 }
 
 fn find_imports<'a>(
index c7203451ca6a700bcd9ce84318e3fb5f6ff1cb95..5e4c22bc5c88c1e7e28303b293655c54a8f67bd7 100644 (file)
@@ -1,6 +1,7 @@
 //! Advertizes the capabilities of the LSP Server.
 use std::env;
 
+use ide::CompletionResolveCapability;
 use lsp_types::{
     CallHierarchyServerCapability, ClientCapabilities, CodeActionKind, CodeActionOptions,
     CodeActionProviderCapability, CodeLensOptions, CompletionOptions,
@@ -11,6 +12,7 @@
     TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability,
     WorkDoneProgressOptions,
 };
+use rustc_hash::FxHashSet;
 use serde_json::json;
 
 use crate::semantic_tokens;
@@ -30,7 +32,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
         })),
         hover_provider: Some(HoverProviderCapability::Simple(true)),
         completion_provider: Some(CompletionOptions {
-            resolve_provider: None,
+            resolve_provider: completions_resolve_provider(client_caps),
             trigger_characters: Some(vec![":".to_string(), ".".to_string()]),
             work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
         }),
@@ -93,6 +95,40 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
     }
 }
 
+fn completions_resolve_provider(client_caps: &ClientCapabilities) -> Option<bool> {
+    if enabled_completions_resolve_capabilities(client_caps)?.is_empty() {
+        log::info!("No `additionalTextEdits` completion resolve capability was found in the client capabilities, autoimport completion is disabled");
+        None
+    } else {
+        Some(true)
+    }
+}
+
+/// Parses client capabilities and returns all completion resolve capabilities rust-analyzer supports.
+pub(crate) fn enabled_completions_resolve_capabilities(
+    caps: &ClientCapabilities,
+) -> Option<FxHashSet<CompletionResolveCapability>> {
+    Some(
+        caps.text_document
+            .as_ref()?
+            .completion
+            .as_ref()?
+            .completion_item
+            .as_ref()?
+            .resolve_support
+            .as_ref()?
+            .properties
+            .iter()
+            .filter_map(|cap_string| match cap_string.as_str() {
+                "additionalTextEdits" => Some(CompletionResolveCapability::AdditionalTextEdits),
+                "detail" => Some(CompletionResolveCapability::Detail),
+                "documentation" => Some(CompletionResolveCapability::Documentation),
+                _unsupported => None,
+            })
+            .collect(),
+    )
+}
+
 fn code_action_capabilities(client_caps: &ClientCapabilities) -> CodeActionProviderCapability {
     client_caps
         .text_document
index 59269a74b918f72cff7218c72c1a23d84dd59c54..5243b50c801c47e71d19e942c381280e2f5ff618 100644 (file)
@@ -19,7 +19,7 @@
 use serde::Deserialize;
 use vfs::AbsPathBuf;
 
-use crate::diagnostics::DiagnosticsMapConfig;
+use crate::{caps::enabled_completions_resolve_capabilities, diagnostics::DiagnosticsMapConfig};
 
 #[derive(Debug, Clone)]
 pub struct Config {
@@ -182,7 +182,7 @@ pub fn new(root_path: AbsPathBuf) -> Self {
             },
             completion: CompletionConfig {
                 enable_postfix_completions: true,
-                enable_experimental_completions: true,
+                enable_autoimport_completions: true,
                 add_call_parenthesis: true,
                 add_call_argument_snippets: true,
                 ..CompletionConfig::default()
@@ -305,7 +305,7 @@ pub fn update(&mut self, json: serde_json::Value) {
         };
 
         self.completion.enable_postfix_completions = data.completion_postfix_enable;
-        self.completion.enable_experimental_completions = data.completion_enableExperimental;
+        self.completion.enable_autoimport_completions = data.completion_autoimport_enable;
         self.completion.add_call_parenthesis = data.completion_addCallParenthesis;
         self.completion.add_call_argument_snippets = data.completion_addCallArgumentSnippets;
         self.completion.merge = self.assist.insert_use.merge;
@@ -388,6 +388,8 @@ pub fn update_caps(&mut self, caps: &ClientCapabilities) {
             }
 
             self.completion.allow_snippets(false);
+            self.completion.active_resolve_capabilities =
+                enabled_completions_resolve_capabilities(caps).unwrap_or_default();
             if let Some(completion) = &doc_caps.completion {
                 if let Some(completion_item) = &completion.completion_item {
                     if let Some(value) = completion_item.snippet_support {
@@ -506,7 +508,7 @@ struct ConfigData {
         completion_addCallArgumentSnippets: bool = true,
         completion_addCallParenthesis: bool      = true,
         completion_postfix_enable: bool          = true,
-        completion_enableExperimental: bool      = true,
+        completion_autoimport_enable: bool       = true,
 
         diagnostics_enable: bool                = true,
         diagnostics_enableExperimental: bool    = true,
index 1cf4139d2bb30e1a31525f212518d5fd28c33593..89c7fd2c7ba2048d3818c635a890f7d69fa0d775 100644 (file)
@@ -8,8 +8,8 @@
 };
 
 use ide::{
-    FileId, FilePosition, FileRange, HoverAction, HoverGotoTypeData, NavigationTarget, Query,
-    RangeInfo, Runnable, RunnableKind, SearchScope, TextEdit,
+    CompletionResolveCapability, FileId, FilePosition, FileRange, HoverAction, HoverGotoTypeData,
+    NavigationTarget, Query, RangeInfo, Runnable, RunnableKind, SearchScope, TextEdit,
 };
 use itertools::Itertools;
 use lsp_server::ErrorCode;
@@ -21,7 +21,7 @@
     HoverContents, Location, NumberOrString, Position, PrepareRenameResponse, Range, RenameParams,
     SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, SemanticTokensParams,
     SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
-    SymbolTag, TextDocumentIdentifier, Url, WorkspaceEdit,
+    SymbolTag, TextDocumentIdentifier, TextDocumentPositionParams, Url, WorkspaceEdit,
 };
 use project_model::TargetKind;
 use serde::{Deserialize, Serialize};
@@ -35,6 +35,7 @@
     from_json, from_proto,
     global_state::{GlobalState, GlobalStateSnapshot},
     lsp_ext::{self, InlayHint, InlayHintsParams},
+    lsp_utils::all_edits_are_disjoint,
     to_proto, LspError, Result,
 };
 
@@ -539,6 +540,7 @@ pub(crate) fn handle_completion(
     params: lsp_types::CompletionParams,
 ) -> Result<Option<lsp_types::CompletionResponse>> {
     let _p = profile::span("handle_completion");
+    let text_document_position = params.text_document_position.clone();
     let position = from_proto::file_position(&snap, params.text_document_position)?;
     let completion_triggered_after_single_colon = {
         let mut res = false;
@@ -568,15 +570,99 @@ pub(crate) fn handle_completion(
     };
     let line_index = snap.analysis.file_line_index(position.file_id)?;
     let line_endings = snap.file_line_endings(position.file_id);
+
     let items: Vec<CompletionItem> = items
         .into_iter()
-        .flat_map(|item| to_proto::completion_item(&line_index, line_endings, item))
+        .flat_map(|item| {
+            let mut new_completion_items =
+                to_proto::completion_item(&line_index, line_endings, item.clone());
+
+            if snap.config.completion.resolve_additional_edits_lazily() {
+                for new_item in &mut new_completion_items {
+                    let _ = fill_resolve_data(&mut new_item.data, &item, &text_document_position)
+                        .take();
+                }
+            }
+
+            new_completion_items
+        })
         .collect();
 
     let completion_list = lsp_types::CompletionList { is_incomplete: true, items };
     Ok(Some(completion_list.into()))
 }
 
+pub(crate) fn handle_completion_resolve(
+    snap: GlobalStateSnapshot,
+    mut original_completion: CompletionItem,
+) -> Result<CompletionItem> {
+    let _p = profile::span("handle_completion_resolve");
+
+    if !all_edits_are_disjoint(&original_completion, &[]) {
+        return Err(LspError::new(
+            ErrorCode::InvalidParams as i32,
+            "Received a completion with overlapping edits, this is not LSP-compliant".into(),
+        )
+        .into());
+    }
+
+    // FIXME resolve the other capabilities also?
+    if !snap
+        .config
+        .completion
+        .active_resolve_capabilities
+        .contains(&CompletionResolveCapability::AdditionalTextEdits)
+    {
+        return Ok(original_completion);
+    }
+
+    let resolve_data = match original_completion
+        .data
+        .take()
+        .map(|data| serde_json::from_value::<CompletionResolveData>(data))
+        .transpose()?
+    {
+        Some(data) => data,
+        None => return Ok(original_completion),
+    };
+
+    let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)?;
+    let line_index = snap.analysis.file_line_index(file_id)?;
+    let line_endings = snap.file_line_endings(file_id);
+    let offset = from_proto::offset(&line_index, resolve_data.position.position);
+
+    let additional_edits = snap
+        .analysis
+        .resolve_completion_edits(
+            &snap.config.completion,
+            FilePosition { file_id, offset },
+            &resolve_data.full_import_path,
+            &resolve_data.imported_name,
+        )?
+        .into_iter()
+        .flat_map(|edit| {
+            edit.into_iter().map(|indel| to_proto::text_edit(&line_index, line_endings, indel))
+        })
+        .collect_vec();
+
+    if !all_edits_are_disjoint(&original_completion, &additional_edits) {
+        return Err(LspError::new(
+            ErrorCode::InternalError as i32,
+            "Import edit overlaps with the original completion edits, this is not LSP-compliant"
+                .into(),
+        )
+        .into());
+    }
+
+    if let Some(original_additional_edits) = original_completion.additional_text_edits.as_mut() {
+        original_additional_edits.extend(additional_edits.into_iter())
+    } else {
+        original_completion.additional_text_edits = Some(additional_edits);
+    }
+
+    Ok(original_completion)
+}
+
 pub(crate) fn handle_folding_range(
     snap: GlobalStateSnapshot,
     params: FoldingRangeParams,
@@ -1534,3 +1620,30 @@ fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>)
         _ => false,
     }
 }
+
+#[derive(Debug, Serialize, Deserialize)]
+struct CompletionResolveData {
+    position: lsp_types::TextDocumentPositionParams,
+    full_import_path: String,
+    imported_name: String,
+}
+
+fn fill_resolve_data(
+    resolve_data: &mut Option<serde_json::Value>,
+    item: &ide::CompletionItem,
+    position: &TextDocumentPositionParams,
+) -> Option<()> {
+    let import_edit = item.import_to_add()?;
+    let full_import_path = import_edit.import_path.to_string();
+    let imported_name = import_edit.import_path.segments.clone().pop()?.to_string();
+
+    *resolve_data = Some(
+        to_value(CompletionResolveData {
+            position: position.to_owned(),
+            full_import_path,
+            imported_name,
+        })
+        .unwrap(),
+    );
+    Some(())
+}
index 6427c73672d6fdcd311cfb32c1241e5876ff7507..60c12e4e2eacfa9cb5a0cbc1113dd4ff7fc72264 100644 (file)
@@ -129,9 +129,40 @@ fn covers(&self, line: u32) -> bool {
     }
 }
 
+/// Checks that the edits inside the completion and the additional edits do not overlap.
+/// LSP explicitly forbits the additional edits to overlap both with the main edit and themselves.
+pub(crate) fn all_edits_are_disjoint(
+    completion: &lsp_types::CompletionItem,
+    additional_edits: &[lsp_types::TextEdit],
+) -> bool {
+    let mut edit_ranges = Vec::new();
+    match completion.text_edit.as_ref() {
+        Some(lsp_types::CompletionTextEdit::Edit(edit)) => {
+            edit_ranges.push(edit.range);
+        }
+        Some(lsp_types::CompletionTextEdit::InsertAndReplace(edit)) => {
+            edit_ranges.push(edit.insert);
+            edit_ranges.push(edit.replace);
+        }
+        None => {}
+    }
+    if let Some(additional_changes) = completion.additional_text_edits.as_ref() {
+        edit_ranges.extend(additional_changes.iter().map(|edit| edit.range));
+    };
+    edit_ranges.extend(additional_edits.iter().map(|edit| edit.range));
+    edit_ranges.sort_by_key(|range| (range.start, range.end));
+    edit_ranges
+        .iter()
+        .zip(edit_ranges.iter().skip(1))
+        .all(|(previous, next)| previous.end <= next.start)
+}
+
 #[cfg(test)]
 mod tests {
-    use lsp_types::{Position, Range, TextDocumentContentChangeEvent};
+    use lsp_types::{
+        CompletionItem, CompletionTextEdit, InsertReplaceEdit, Position, Range,
+        TextDocumentContentChangeEvent,
+    };
 
     use super::*;
 
@@ -197,4 +228,135 @@ macro_rules! c {
         apply_document_changes(&mut text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]);
         assert_eq!(text, "ațc\ncb");
     }
+
+    #[test]
+    fn empty_completion_disjoint_tests() {
+        let empty_completion =
+            CompletionItem::new_simple("label".to_string(), "detail".to_string());
+
+        let disjoint_edit_1 = lsp_types::TextEdit::new(
+            Range::new(Position::new(2, 2), Position::new(3, 3)),
+            "new_text".to_string(),
+        );
+        let disjoint_edit_2 = lsp_types::TextEdit::new(
+            Range::new(Position::new(3, 3), Position::new(4, 4)),
+            "new_text".to_string(),
+        );
+
+        let joint_edit = lsp_types::TextEdit::new(
+            Range::new(Position::new(1, 1), Position::new(5, 5)),
+            "new_text".to_string(),
+        );
+
+        assert!(
+            all_edits_are_disjoint(&empty_completion, &[]),
+            "Empty completion has all its edits disjoint"
+        );
+        assert!(
+            all_edits_are_disjoint(
+                &empty_completion,
+                &[disjoint_edit_1.clone(), disjoint_edit_2.clone()]
+            ),
+            "Empty completion is disjoint to whatever disjoint extra edits added"
+        );
+
+        assert!(
+            !all_edits_are_disjoint(
+                &empty_completion,
+                &[disjoint_edit_1, disjoint_edit_2, joint_edit]
+            ),
+            "Empty completion does not prevent joint extra edits from failing the validation"
+        );
+    }
+
+    #[test]
+    fn completion_with_joint_edits_disjoint_tests() {
+        let disjoint_edit = lsp_types::TextEdit::new(
+            Range::new(Position::new(1, 1), Position::new(2, 2)),
+            "new_text".to_string(),
+        );
+        let disjoint_edit_2 = lsp_types::TextEdit::new(
+            Range::new(Position::new(2, 2), Position::new(3, 3)),
+            "new_text".to_string(),
+        );
+        let joint_edit = lsp_types::TextEdit::new(
+            Range::new(Position::new(1, 1), Position::new(5, 5)),
+            "new_text".to_string(),
+        );
+
+        let mut completion_with_joint_edits =
+            CompletionItem::new_simple("label".to_string(), "detail".to_string());
+        completion_with_joint_edits.additional_text_edits =
+            Some(vec![disjoint_edit.clone(), joint_edit.clone()]);
+        assert!(
+            !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+            "Completion with disjoint edits fails the validaton even with empty extra edits"
+        );
+
+        completion_with_joint_edits.text_edit =
+            Some(CompletionTextEdit::Edit(disjoint_edit.clone()));
+        completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit.clone()]);
+        assert!(
+            !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+            "Completion with disjoint edits fails the validaton even with empty extra edits"
+        );
+
+        completion_with_joint_edits.text_edit =
+            Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
+                new_text: "new_text".to_string(),
+                insert: disjoint_edit.range,
+                replace: joint_edit.range,
+            }));
+        completion_with_joint_edits.additional_text_edits = None;
+        assert!(
+            !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+            "Completion with disjoint edits fails the validaton even with empty extra edits"
+        );
+
+        completion_with_joint_edits.text_edit =
+            Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
+                new_text: "new_text".to_string(),
+                insert: disjoint_edit.range,
+                replace: disjoint_edit_2.range,
+            }));
+        completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit]);
+        assert!(
+            !all_edits_are_disjoint(&completion_with_joint_edits, &[]),
+            "Completion with disjoint edits fails the validaton even with empty extra edits"
+        );
+    }
+
+    #[test]
+    fn completion_with_disjoint_edits_disjoint_tests() {
+        let disjoint_edit = lsp_types::TextEdit::new(
+            Range::new(Position::new(1, 1), Position::new(2, 2)),
+            "new_text".to_string(),
+        );
+        let disjoint_edit_2 = lsp_types::TextEdit::new(
+            Range::new(Position::new(2, 2), Position::new(3, 3)),
+            "new_text".to_string(),
+        );
+        let joint_edit = lsp_types::TextEdit::new(
+            Range::new(Position::new(1, 1), Position::new(5, 5)),
+            "new_text".to_string(),
+        );
+
+        let mut completion_with_disjoint_edits =
+            CompletionItem::new_simple("label".to_string(), "detail".to_string());
+        completion_with_disjoint_edits.text_edit = Some(CompletionTextEdit::Edit(disjoint_edit));
+        let completion_with_disjoint_edits = completion_with_disjoint_edits;
+
+        assert!(
+            all_edits_are_disjoint(&completion_with_disjoint_edits, &[]),
+            "Completion with disjoint edits is valid"
+        );
+        assert!(
+            !all_edits_are_disjoint(&completion_with_disjoint_edits, &[joint_edit.clone()]),
+            "Completion with disjoint edits and joint extra edit is invalid"
+        );
+        assert!(
+            all_edits_are_disjoint(&completion_with_disjoint_edits, &[disjoint_edit_2.clone()]),
+            "Completion with disjoint edits and joint extra edit is valid"
+        );
+    }
 }
index 55d46b09e78c7ffedc96b27a7321bc40ddec28ae..95be2ebd397e81aace24ca7da9ef316e527f74f8 100644 (file)
@@ -454,6 +454,7 @@ fn on_request(&mut self, request_received: Instant, req: Request) -> Result<()>
             .on::<lsp_types::request::GotoImplementation>(handlers::handle_goto_implementation)
             .on::<lsp_types::request::GotoTypeDefinition>(handlers::handle_goto_type_definition)
             .on::<lsp_types::request::Completion>(handlers::handle_completion)
+            .on::<lsp_types::request::ResolveCompletionItem>(handlers::handle_completion_resolve)
             .on::<lsp_types::request::CodeLensRequest>(handlers::handle_code_lens)
             .on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)
             .on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)
index af228f98370435f1e705281e77df5929ea3fb1a0..dbde37005c92aae22d848b26f6e6e20c3be41853 100644 (file)
                     "default": true,
                     "markdownDescription": "Whether to show postfix snippets like `dbg`, `if`, `not`, etc."
                 },
-                "rust-analyzer.completion.enableExperimental": {
+                "rust-analyzer.completion.autoimport.enable": {
                     "type": "boolean",
                     "default": true,
-                    "markdownDescription": "Display additional completions with potential false positives and performance issues"
+                    "markdownDescription": [
+                        "Toggles the additional completions that automatically add imports when completed.",
+                        "Note that your client have to specify the `additionalTextEdits` LSP client capability to truly have this feature enabled"
+                    ]
                 },
                 "rust-analyzer.callInfo.full": {
                     "type": "boolean",