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, pub merge: Option, + /// A set of capabilities, enabled on the client and supported on the server. + pub active_resolve_capabilities: FxHashSet, +} + +/// 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, } // 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, +#[derive(Debug, Clone)] +pub struct ImportEdit { + pub import_path: ModPath, + pub import_scope: ImportScope, + pub merge_behaviour: Option, +} + +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 { + 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, + import_to_add: Option, label: String, insert_text: Option, 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) -> 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) -> Builder { + pub(crate) fn add_import(mut self, import_to_add: Option) -> 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> { + 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, + import_edit: ImportEdit, resolution: &ScopeDef, ) -> Option { - 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, + import_to_add: Option, resolution: &ScopeDef, ) -> Option { 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, + import_to_add: Option, local_name: Option, variant: hir::EnumVariant, path: Option, @@ -62,7 +62,7 @@ impl<'a> EnumVariantRender<'a> { } } - fn render(self, import_to_add: Option) -> CompletionItem { + fn render(self, import_to_add: Option) -> 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, + import_to_add: Option, local_name: Option, 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) -> CompletionItem { + fn render(self, import_to_add: Option) -> 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, + import_to_add: Option, name: String, macro_: hir::MacroDef, ) -> Option { @@ -38,7 +38,7 @@ impl<'a> MacroRender<'a> { MacroRender { ctx, name, macro_, docs, bra, ket } } - fn render(&self, import_to_add: Option) -> Option { + fn render(&self, import_to_add: Option) -> Option { // 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/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) } 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> { + 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, name_to_import: &str, - limit: usize, ignore_modules: bool, ) -> impl Iterator> { 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 { + 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> { + 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> { 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 = 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 { + 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::(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, + 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::(handlers::handle_goto_implementation) .on::(handlers::handle_goto_type_definition) .on::(handlers::handle_completion) + .on::(handlers::handle_completion_resolve) .on::(handlers::handle_code_lens) .on::(handlers::handle_code_lens_resolve) .on::(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",