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>
This commit is contained in:
bors[bot] 2020-12-08 13:10:28 +00:00 committed by GitHub
commit 4d4f11925f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 565 additions and 118 deletions

View file

@ -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

View 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(),
}
}
}

View file

@ -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
}

View file

@ -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;

View file

@ -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,

View file

@ -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(),

View file

@ -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())

View file

@ -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() {

View 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)
}

View file

@ -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,

View 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>(

View 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 @@ 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

View file

@ -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,

View file

@ -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(())
}

View file

@ -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"
);
}
}

View file

@ -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)

View file

@ -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",