Repository: rust-analyzer/rust-analyzer
Pull request #6706 (2020-12-03 10:07:06)

Move import text edit calculation into a completion resolve request
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

SomeoneToIgnore: While we at it, we can discuss the autocompletion flag name: https://github.com/rust-analyzer/rust-analyzer/pull/6631#issuecomment-733600165
kjeremy: I think this looks pretty good but would like another set of eyes on it. Can we add some tests that do not go through the LSP layer?
SomeoneToIgnore: > Can we add some tests that do not go through the LSP layer?

They kind of function this way already by doing that eager import application, but I've added one more to check the completion when we have the corresponding resolve capability in the completion settings.
Not sure what other non-LSP tests I can add.

Patch:
diff --git a/crates/completion/src/completions/unqualified_path.rs b/crates/completion/src/completions/unqualified_path.rs
index 81691cd7f2..4e4e2b36f0 100644
--- a/crates/completion/src/completions/unqualified_path.rs
+++ b/crates/completion/src/completions/unqualified_path.rs
@@ -9,7 +9,7 @@ use test_utils::mark;
 
 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
diff --git a/crates/completion/src/config.rs b/crates/completion/src/config.rs
index 654a76f7b3..5175b9d69d 100644
--- a/crates/completion/src/config.rs
+++ b/crates/completion/src/config.rs
@@ -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(),
         }
     }
 }
diff --git a/crates/completion/src/item.rs b/crates/completion/src/item.rs
index e85549fef4..bd94402d75 100644
--- a/crates/completion/src/item.rs
+++ b/crates/completion/src/item.rs
@@ -15,6 +15,7 @@ use crate::config::SnippetCap;
 /// `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 @@ impl CompletionItem {
     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 @@ impl Builder {
         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 @@ impl Builder {
                 }
                 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 @@ impl Builder {
             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 @@ impl 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
     }
diff --git a/crates/completion/src/lib.rs b/crates/completion/src/lib.rs
index 1ec2e9be72..f60f87243f 100644
--- a/crates/completion/src/lib.rs
+++ b/crates/completion/src/lib.rs
@@ -11,14 +11,17 @@ mod render;
 
 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.
@@ -70,12 +73,9 @@ pub use crate::{
 // }
 // ```
 //
-// 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;
diff --git a/crates/completion/src/render.rs b/crates/completion/src/render.rs
index 504757a6ae..b940388df2 100644
--- a/crates/completion/src/render.rs
+++ b/crates/completion/src/render.rs
@@ -9,14 +9,13 @@ pub(crate) mod type_alias;
 
 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 @@ impl<'a> Render<'a> {
     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,
diff --git a/crates/completion/src/render/enum_variant.rs b/crates/completion/src/render/enum_variant.rs
index f4bd02f258..8e0fea6c0f 100644
--- a/crates/completion/src/render/enum_variant.rs
+++ b/crates/completion/src/render/enum_variant.rs
@@ -5,13 +5,13 @@ use itertools::Itertools;
 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 @@ impl<'a> EnumVariantRender<'a> {
         }
     }
 
-    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(),
diff --git a/crates/completion/src/render/function.rs b/crates/completion/src/render/function.rs
index 00e3eb203e..d16005249c 100644
--- a/crates/completion/src/render/function.rs
+++ b/crates/completion/src/render/function.rs
@@ -5,13 +5,13 @@ use syntax::{ast::Fn, display::function_declaration};
 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 @@ impl<'a> FunctionRender<'a> {
         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())
diff --git a/crates/completion/src/render/macro_.rs b/crates/completion/src/render/macro_.rs
index b4ab32c6e4..eb3209bee3 100644
--- a/crates/completion/src/render/macro_.rs
+++ b/crates/completion/src/render/macro_.rs
@@ -5,13 +5,13 @@ use syntax::display::macro_label;
 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 @@ impl<'a> MacroRender<'a> {
         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() {
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 5244bdd610..71068cac28 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -80,7 +80,8 @@ pub use crate::{
     },
 };
 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 @@ impl Analysis {
         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,
diff --git a/crates/ide_db/src/imports_locator.rs b/crates/ide_db/src/imports_locator.rs
index 09046d3c36..b2980a5d69 100644
--- a/crates/ide_db/src/imports_locator.rs
+++ b/crates/ide_db/src/imports_locator.rs
@@ -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>(
diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs
index c7203451ca..5e4c22bc5c 100644
--- a/crates/rust-analyzer/src/caps.rs
+++ b/crates/rust-analyzer/src/caps.rs
@@ -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 @@ use lsp_types::{
     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
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 59269a74b9..5243b50c80 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -19,7 +19,7 @@ use rustc_hash::FxHashSet;
 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 @@ impl Config {
             },
             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 @@ impl Config {
         };
 
         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 @@ impl Config {
             }
 
             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 @@ config_data! {
         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,
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 1cf4139d2b..89c7fd2c7b 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -8,8 +8,8 @@ use std::{
 };
 
 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 @@ use lsp_types::{
     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 @@ use crate::{
     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(())
+}
diff --git a/crates/rust-analyzer/src/lsp_utils.rs b/crates/rust-analyzer/src/lsp_utils.rs
index 6427c73672..60c12e4e2e 100644
--- a/crates/rust-analyzer/src/lsp_utils.rs
+++ b/crates/rust-analyzer/src/lsp_utils.rs
@@ -129,9 +129,40 @@ pub(crate) fn apply_document_changes(
     }
 }
 
+/// 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 @@ mod tests {
         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"
+        );
+    }
 }
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 55d46b09e7..95be2ebd39 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -454,6 +454,7 @@ impl GlobalState {
             .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)
diff --git a/editors/code/package.json b/editors/code/package.json
index af228f9837..dbde37005c 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -460,10 +460,13 @@
                     "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",


Test patch:
diff --git a/crates/completion/src/test_utils.rs b/crates/completion/src/test_utils.rs
index 4c1b1a8392..25f5f4924c 100644
--- a/crates/completion/src/test_utils.rs
+++ b/crates/completion/src/test_utils.rs
@@ -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)
 }