mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
Merge #6706
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:
commit
4d4f11925f
17 changed files with 565 additions and 118 deletions
|
@ -9,7 +9,7 @@ use test_utils::mark;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
render::{render_resolution_with_import, RenderContext},
|
render::{render_resolution_with_import, RenderContext},
|
||||||
CompletionContext, Completions,
|
CompletionContext, Completions, ImportEdit,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionContext) {
|
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)
|
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()
|
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<()> {
|
fn fuzzy_completion(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
|
||||||
let _p = profile::span("fuzzy_completion");
|
let _p = profile::span("fuzzy_completion");
|
||||||
|
let potential_import_name = ctx.token.to_string();
|
||||||
|
|
||||||
let current_module = ctx.scope.module()?;
|
let current_module = ctx.scope.module()?;
|
||||||
let anchor = ctx.name_ref_syntax.as_ref()?;
|
let anchor = ctx.name_ref_syntax.as_ref()?;
|
||||||
let import_scope = ImportScope::find_insert_use_container(anchor.syntax(), &ctx.sema)?;
|
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(
|
let possible_imports = imports_locator::find_similar_imports(
|
||||||
&ctx.sema,
|
&ctx.sema,
|
||||||
ctx.krate?,
|
ctx.krate?,
|
||||||
|
Some(100),
|
||||||
&potential_import_name,
|
&potential_import_name,
|
||||||
50,
|
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.filter_map(|import_candidate| {
|
.filter_map(|import_candidate| {
|
||||||
|
@ -99,13 +144,14 @@ fn fuzzy_completion(acc: &mut Completions, ctx: &CompletionContext) -> Option<()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.filter(|(mod_path, _)| mod_path.len() > 1)
|
.filter(|(mod_path, _)| mod_path.len() > 1)
|
||||||
.take(20)
|
|
||||||
.filter_map(|(import_path, definition)| {
|
.filter_map(|(import_path, definition)| {
|
||||||
render_resolution_with_import(
|
render_resolution_with_import(
|
||||||
RenderContext::new(ctx),
|
RenderContext::new(ctx),
|
||||||
import_path.clone(),
|
ImportEdit {
|
||||||
import_scope.clone(),
|
import_path: import_path.clone(),
|
||||||
ctx.config.merge,
|
import_scope: import_scope.clone(),
|
||||||
|
merge_behaviour: ctx.config.merge,
|
||||||
|
},
|
||||||
&definition,
|
&definition,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -120,8 +166,8 @@ mod tests {
|
||||||
use test_utils::mark;
|
use test_utils::mark;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
test_utils::{check_edit, completion_list},
|
test_utils::{check_edit, check_edit_with_config, completion_list},
|
||||||
CompletionKind,
|
CompletionConfig, CompletionKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn check(ra_fixture: &str, expect: Expect) {
|
fn check(ra_fixture: &str, expect: Expect) {
|
||||||
|
@ -730,7 +776,13 @@ impl My<|>
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn function_fuzzy_completion() {
|
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",
|
"stdin",
|
||||||
r#"
|
r#"
|
||||||
//- /lib.rs crate:dep
|
//- /lib.rs crate:dep
|
||||||
|
@ -755,7 +807,13 @@ fn main() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn macro_fuzzy_completion() {
|
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!",
|
"macro_with_curlies!",
|
||||||
r#"
|
r#"
|
||||||
//- /lib.rs crate:dep
|
//- /lib.rs crate:dep
|
||||||
|
@ -782,7 +840,13 @@ fn main() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn struct_fuzzy_completion() {
|
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",
|
"ThirdStruct",
|
||||||
r#"
|
r#"
|
||||||
//- /lib.rs crate:dep
|
//- /lib.rs crate:dep
|
||||||
|
|
|
@ -5,21 +5,42 @@
|
||||||
//! completions if we are allowed to.
|
//! completions if we are allowed to.
|
||||||
|
|
||||||
use ide_db::helpers::insert_use::MergeBehaviour;
|
use ide_db::helpers::insert_use::MergeBehaviour;
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct CompletionConfig {
|
pub struct CompletionConfig {
|
||||||
pub enable_postfix_completions: bool,
|
pub enable_postfix_completions: bool,
|
||||||
pub enable_experimental_completions: bool,
|
pub enable_autoimport_completions: bool,
|
||||||
pub add_call_parenthesis: bool,
|
pub add_call_parenthesis: bool,
|
||||||
pub add_call_argument_snippets: bool,
|
pub add_call_argument_snippets: bool,
|
||||||
pub snippet_cap: Option<SnippetCap>,
|
pub snippet_cap: Option<SnippetCap>,
|
||||||
pub merge: Option<MergeBehaviour>,
|
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 {
|
impl CompletionConfig {
|
||||||
pub fn allow_snippets(&mut self, yes: bool) {
|
pub fn allow_snippets(&mut self, yes: bool) {
|
||||||
self.snippet_cap = if yes { Some(SnippetCap { _private: () }) } else { None }
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
@ -31,11 +52,12 @@ impl Default for CompletionConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
CompletionConfig {
|
CompletionConfig {
|
||||||
enable_postfix_completions: true,
|
enable_postfix_completions: true,
|
||||||
enable_experimental_completions: true,
|
enable_autoimport_completions: true,
|
||||||
add_call_parenthesis: true,
|
add_call_parenthesis: true,
|
||||||
add_call_argument_snippets: true,
|
add_call_argument_snippets: true,
|
||||||
snippet_cap: Some(SnippetCap { _private: () }),
|
snippet_cap: Some(SnippetCap { _private: () }),
|
||||||
merge: Some(MergeBehaviour::Full),
|
merge: Some(MergeBehaviour::Full),
|
||||||
|
active_resolve_capabilities: FxHashSet::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ use crate::config::SnippetCap;
|
||||||
/// `CompletionItem` describes a single completion variant in the editor pop-up.
|
/// `CompletionItem` describes a single completion variant in the editor pop-up.
|
||||||
/// It is basically a POD with various properties. To construct a
|
/// It is basically a POD with various properties. To construct a
|
||||||
/// `CompletionItem`, use `new` method and the `Builder` struct.
|
/// `CompletionItem`, use `new` method and the `Builder` struct.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct CompletionItem {
|
pub struct CompletionItem {
|
||||||
/// Used only internally in tests, to check only specific kind of
|
/// Used only internally in tests, to check only specific kind of
|
||||||
/// completion (postfix, keyword, reference, etc).
|
/// completion (postfix, keyword, reference, etc).
|
||||||
|
@ -65,6 +66,9 @@ pub struct CompletionItem {
|
||||||
/// Indicates that a reference or mutable reference to this variable is a
|
/// Indicates that a reference or mutable reference to this variable is a
|
||||||
/// possible match.
|
/// possible match.
|
||||||
ref_match: Option<(Mutability, CompletionScore)>,
|
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.
|
// 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)> {
|
pub fn ref_match(&self) -> Option<(Mutability, CompletionScore)> {
|
||||||
self.ref_match
|
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.
|
/// An extra import to add after the completion is applied.
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ImportToAdd {
|
pub struct ImportEdit {
|
||||||
pub(crate) import_path: ModPath,
|
pub import_path: ModPath,
|
||||||
pub(crate) import_scope: ImportScope,
|
pub import_scope: ImportScope,
|
||||||
pub(crate) merge_behaviour: Option<MergeBehaviour>,
|
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.
|
/// A helper to make `CompletionItem`s.
|
||||||
|
@ -272,7 +299,7 @@ pub(crate) struct ImportToAdd {
|
||||||
pub(crate) struct Builder {
|
pub(crate) struct Builder {
|
||||||
source_range: TextRange,
|
source_range: TextRange,
|
||||||
completion_kind: CompletionKind,
|
completion_kind: CompletionKind,
|
||||||
import_to_add: Option<ImportToAdd>,
|
import_to_add: Option<ImportEdit>,
|
||||||
label: String,
|
label: String,
|
||||||
insert_text: Option<String>,
|
insert_text: Option<String>,
|
||||||
insert_text_format: InsertTextFormat,
|
insert_text_format: InsertTextFormat,
|
||||||
|
@ -294,11 +321,9 @@ impl Builder {
|
||||||
let mut label = self.label;
|
let mut label = self.label;
|
||||||
let mut lookup = self.lookup;
|
let mut lookup = self.lookup;
|
||||||
let mut insert_text = self.insert_text;
|
let mut insert_text = self.insert_text;
|
||||||
let mut text_edits = TextEdit::builder();
|
|
||||||
|
|
||||||
if let Some(import_data) = self.import_to_add {
|
if let Some(import_to_add) = self.import_to_add.as_ref() {
|
||||||
let import = mod_path_to_ast(&import_data.import_path);
|
let mut import_path_without_last_segment = import_to_add.import_path.to_owned();
|
||||||
let mut import_path_without_last_segment = import_data.import_path;
|
|
||||||
let _ = import_path_without_last_segment.segments.pop();
|
let _ = import_path_without_last_segment.segments.pop();
|
||||||
|
|
||||||
if !import_path_without_last_segment.segments.is_empty() {
|
if !import_path_without_last_segment.segments.is_empty() {
|
||||||
|
@ -310,32 +335,20 @@ impl Builder {
|
||||||
}
|
}
|
||||||
label = format!("{}::{}", import_path_without_last_segment, label);
|
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,
|
Some(it) => it,
|
||||||
None => {
|
None => {
|
||||||
TextEdit::replace(self.source_range, insert_text.unwrap_or_else(|| label.clone()))
|
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 {
|
CompletionItem {
|
||||||
source_range: self.source_range,
|
source_range: self.source_range,
|
||||||
label,
|
label,
|
||||||
insert_text_format: self.insert_text_format,
|
insert_text_format: self.insert_text_format,
|
||||||
text_edit: resulting_edit,
|
text_edit,
|
||||||
detail: self.detail,
|
detail: self.detail,
|
||||||
documentation: self.documentation,
|
documentation: self.documentation,
|
||||||
lookup,
|
lookup,
|
||||||
|
@ -345,6 +358,7 @@ impl Builder {
|
||||||
trigger_call_info: self.trigger_call_info.unwrap_or(false),
|
trigger_call_info: self.trigger_call_info.unwrap_or(false),
|
||||||
score: self.score,
|
score: self.score,
|
||||||
ref_match: self.ref_match,
|
ref_match: self.ref_match,
|
||||||
|
import_to_add: self.import_to_add,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder {
|
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.trigger_call_info = Some(true);
|
||||||
self
|
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.import_to_add = import_to_add;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,17 @@ mod render;
|
||||||
|
|
||||||
mod completions;
|
mod completions;
|
||||||
|
|
||||||
use ide_db::base_db::FilePosition;
|
use ide_db::{
|
||||||
use ide_db::RootDatabase;
|
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};
|
use crate::{completions::Completions, context::CompletionContext, item::CompletionKind};
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
config::CompletionConfig,
|
config::{CompletionConfig, CompletionResolveCapability},
|
||||||
item::{CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat},
|
item::{CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, InsertTextFormat},
|
||||||
};
|
};
|
||||||
|
|
||||||
//FIXME: split the following feature into fine-grained features.
|
//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.
|
// And the auto import completions, enabled with the `rust-analyzer.completion.autoimport.enable` setting and the corresponding LSP client capabilities.
|
||||||
// This flag enables or disables:
|
// Those are the additional completion options with automatic `use` import and options from all project importable items,
|
||||||
//
|
// fuzzy matched agains the completion imput.
|
||||||
// - 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.
|
|
||||||
|
|
||||||
/// Main entry point for completion. We run completion as a two-phase process.
|
/// Main entry point for completion. We run completion as a two-phase process.
|
||||||
///
|
///
|
||||||
|
@ -131,6 +131,33 @@ pub fn completions(
|
||||||
Some(acc)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::config::CompletionConfig;
|
use crate::config::CompletionConfig;
|
||||||
|
|
|
@ -9,14 +9,13 @@ pub(crate) mod type_alias;
|
||||||
|
|
||||||
mod builder_ext;
|
mod builder_ext;
|
||||||
|
|
||||||
use hir::{Documentation, HasAttrs, HirDisplay, ModPath, Mutability, ScopeDef, Type};
|
use hir::{Documentation, HasAttrs, HirDisplay, Mutability, ScopeDef, Type};
|
||||||
use ide_db::helpers::insert_use::{ImportScope, MergeBehaviour};
|
|
||||||
use ide_db::RootDatabase;
|
use ide_db::RootDatabase;
|
||||||
use syntax::TextRange;
|
use syntax::TextRange;
|
||||||
use test_utils::mark;
|
use test_utils::mark;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::SnippetCap, item::ImportToAdd, CompletionContext, CompletionItem, CompletionItemKind,
|
config::SnippetCap, item::ImportEdit, CompletionContext, CompletionItem, CompletionItemKind,
|
||||||
CompletionKind, CompletionScore,
|
CompletionKind, CompletionScore,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,15 +47,12 @@ pub(crate) fn render_resolution<'a>(
|
||||||
|
|
||||||
pub(crate) fn render_resolution_with_import<'a>(
|
pub(crate) fn render_resolution_with_import<'a>(
|
||||||
ctx: RenderContext<'a>,
|
ctx: RenderContext<'a>,
|
||||||
import_path: ModPath,
|
import_edit: ImportEdit,
|
||||||
import_scope: ImportScope,
|
|
||||||
merge_behaviour: Option<MergeBehaviour>,
|
|
||||||
resolution: &ScopeDef,
|
resolution: &ScopeDef,
|
||||||
) -> Option<CompletionItem> {
|
) -> Option<CompletionItem> {
|
||||||
let local_name = import_path.segments.last()?.to_string();
|
|
||||||
Render::new(ctx).render_resolution(
|
Render::new(ctx).render_resolution(
|
||||||
local_name,
|
import_edit.import_path.segments.last()?.to_string(),
|
||||||
Some(ImportToAdd { import_path, import_scope, merge_behaviour }),
|
Some(import_edit),
|
||||||
resolution,
|
resolution,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -147,7 +143,7 @@ impl<'a> Render<'a> {
|
||||||
fn render_resolution(
|
fn render_resolution(
|
||||||
self,
|
self,
|
||||||
local_name: String,
|
local_name: String,
|
||||||
import_to_add: Option<ImportToAdd>,
|
import_to_add: Option<ImportEdit>,
|
||||||
resolution: &ScopeDef,
|
resolution: &ScopeDef,
|
||||||
) -> Option<CompletionItem> {
|
) -> Option<CompletionItem> {
|
||||||
let _p = profile::span("render_resolution");
|
let _p = profile::span("render_resolution");
|
||||||
|
@ -450,28 +446,6 @@ fn main() { let _: m::Spam = S<|> }
|
||||||
insert: "m",
|
insert: "m",
|
||||||
kind: Module,
|
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 {
|
CompletionItem {
|
||||||
label: "m::Spam::Foo",
|
label: "m::Spam::Foo",
|
||||||
source_range: 75..76,
|
source_range: 75..76,
|
||||||
|
|
|
@ -5,13 +5,13 @@ use itertools::Itertools;
|
||||||
use test_utils::mark;
|
use test_utils::mark;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
|
item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
|
||||||
render::{builder_ext::Params, RenderContext},
|
render::{builder_ext::Params, RenderContext},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn render_enum_variant<'a>(
|
pub(crate) fn render_enum_variant<'a>(
|
||||||
ctx: RenderContext<'a>,
|
ctx: RenderContext<'a>,
|
||||||
import_to_add: Option<ImportToAdd>,
|
import_to_add: Option<ImportEdit>,
|
||||||
local_name: Option<String>,
|
local_name: Option<String>,
|
||||||
variant: hir::EnumVariant,
|
variant: hir::EnumVariant,
|
||||||
path: Option<ModPath>,
|
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(
|
let mut builder = CompletionItem::new(
|
||||||
CompletionKind::Reference,
|
CompletionKind::Reference,
|
||||||
self.ctx.source_range(),
|
self.ctx.source_range(),
|
||||||
|
|
|
@ -5,13 +5,13 @@ use syntax::{ast::Fn, display::function_declaration};
|
||||||
use test_utils::mark;
|
use test_utils::mark;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
|
item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
|
||||||
render::{builder_ext::Params, RenderContext},
|
render::{builder_ext::Params, RenderContext},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn render_fn<'a>(
|
pub(crate) fn render_fn<'a>(
|
||||||
ctx: RenderContext<'a>,
|
ctx: RenderContext<'a>,
|
||||||
import_to_add: Option<ImportToAdd>,
|
import_to_add: Option<ImportEdit>,
|
||||||
local_name: Option<String>,
|
local_name: Option<String>,
|
||||||
fn_: hir::Function,
|
fn_: hir::Function,
|
||||||
) -> CompletionItem {
|
) -> CompletionItem {
|
||||||
|
@ -39,7 +39,7 @@ impl<'a> FunctionRender<'a> {
|
||||||
FunctionRender { ctx, name, func: fn_, ast_node }
|
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();
|
let params = self.params();
|
||||||
CompletionItem::new(CompletionKind::Reference, self.ctx.source_range(), self.name.clone())
|
CompletionItem::new(CompletionKind::Reference, self.ctx.source_range(), self.name.clone())
|
||||||
.kind(self.kind())
|
.kind(self.kind())
|
||||||
|
|
|
@ -5,13 +5,13 @@ use syntax::display::macro_label;
|
||||||
use test_utils::mark;
|
use test_utils::mark;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
item::{CompletionItem, CompletionItemKind, CompletionKind, ImportToAdd},
|
item::{CompletionItem, CompletionItemKind, CompletionKind, ImportEdit},
|
||||||
render::RenderContext,
|
render::RenderContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn render_macro<'a>(
|
pub(crate) fn render_macro<'a>(
|
||||||
ctx: RenderContext<'a>,
|
ctx: RenderContext<'a>,
|
||||||
import_to_add: Option<ImportToAdd>,
|
import_to_add: Option<ImportEdit>,
|
||||||
name: String,
|
name: String,
|
||||||
macro_: hir::MacroDef,
|
macro_: hir::MacroDef,
|
||||||
) -> Option<CompletionItem> {
|
) -> Option<CompletionItem> {
|
||||||
|
@ -38,7 +38,7 @@ impl<'a> MacroRender<'a> {
|
||||||
MacroRender { ctx, name, macro_, docs, bra, ket }
|
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,
|
// FIXME: Currently proc-macro do not have ast-node,
|
||||||
// such that it does not have source
|
// such that it does not have source
|
||||||
if self.macro_.is_proc_macro() {
|
if self.macro_.is_proc_macro() {
|
||||||
|
|
|
@ -96,7 +96,16 @@ pub(crate) fn check_edit_with_config(
|
||||||
.collect_tuple()
|
.collect_tuple()
|
||||||
.unwrap_or_else(|| panic!("can't find {:?} completion in {:#?}", what, completions));
|
.unwrap_or_else(|| panic!("can't find {:?} completion in {:#?}", what, completions));
|
||||||
let mut actual = db.file_text(position.file_id).to_string();
|
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)
|
assert_eq_text!(&ra_fixture_after, &actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,8 @@ pub use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
pub use completion::{
|
pub use completion::{
|
||||||
CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat,
|
CompletionConfig, CompletionItem, CompletionItemKind, CompletionResolveCapability,
|
||||||
|
CompletionScore, ImportEdit, InsertTextFormat,
|
||||||
};
|
};
|
||||||
pub use ide_db::{
|
pub use ide_db::{
|
||||||
call_info::CallInfo,
|
call_info::CallInfo,
|
||||||
|
@ -468,6 +469,27 @@ impl Analysis {
|
||||||
self.with_db(|db| completion::completions(db, config, position).map(Into::into))
|
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.
|
/// Computes resolved assists with source changes for the given position.
|
||||||
pub fn resolved_assists(
|
pub fn resolved_assists(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -34,27 +34,25 @@ pub fn find_exact_imports<'a>(
|
||||||
pub fn find_similar_imports<'a>(
|
pub fn find_similar_imports<'a>(
|
||||||
sema: &Semantics<'a, RootDatabase>,
|
sema: &Semantics<'a, RootDatabase>,
|
||||||
krate: Crate,
|
krate: Crate,
|
||||||
|
limit: Option<usize>,
|
||||||
name_to_import: &str,
|
name_to_import: &str,
|
||||||
limit: usize,
|
|
||||||
ignore_modules: bool,
|
ignore_modules: bool,
|
||||||
) -> impl Iterator<Item = Either<ModuleDef, MacroDef>> {
|
) -> impl Iterator<Item = Either<ModuleDef, MacroDef>> {
|
||||||
let _p = profile::span("find_similar_imports");
|
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 {
|
if ignore_modules {
|
||||||
external_query = external_query.exclude_import_kind(import_map::ImportKind::Module);
|
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());
|
let mut local_query = symbol_index::Query::new(name_to_import.to_string());
|
||||||
|
|
||||||
|
if let Some(limit) = limit {
|
||||||
local_query.limit(limit);
|
local_query.limit(limit);
|
||||||
local_query
|
external_query = external_query.limit(limit);
|
||||||
},
|
}
|
||||||
external_query,
|
|
||||||
)
|
find_imports(sema, krate, local_query, external_query)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_imports<'a>(
|
fn find_imports<'a>(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//! Advertizes the capabilities of the LSP Server.
|
//! Advertizes the capabilities of the LSP Server.
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
use ide::CompletionResolveCapability;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
CallHierarchyServerCapability, ClientCapabilities, CodeActionKind, CodeActionOptions,
|
CallHierarchyServerCapability, ClientCapabilities, CodeActionKind, CodeActionOptions,
|
||||||
CodeActionProviderCapability, CodeLensOptions, CompletionOptions,
|
CodeActionProviderCapability, CodeLensOptions, CompletionOptions,
|
||||||
|
@ -11,6 +12,7 @@ use lsp_types::{
|
||||||
TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability,
|
TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability,
|
||||||
WorkDoneProgressOptions,
|
WorkDoneProgressOptions,
|
||||||
};
|
};
|
||||||
|
use rustc_hash::FxHashSet;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::semantic_tokens;
|
use crate::semantic_tokens;
|
||||||
|
@ -30,7 +32,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
|
||||||
})),
|
})),
|
||||||
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||||
completion_provider: Some(CompletionOptions {
|
completion_provider: Some(CompletionOptions {
|
||||||
resolve_provider: None,
|
resolve_provider: completions_resolve_provider(client_caps),
|
||||||
trigger_characters: Some(vec![":".to_string(), ".".to_string()]),
|
trigger_characters: Some(vec![":".to_string(), ".".to_string()]),
|
||||||
work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None },
|
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 {
|
fn code_action_capabilities(client_caps: &ClientCapabilities) -> CodeActionProviderCapability {
|
||||||
client_caps
|
client_caps
|
||||||
.text_document
|
.text_document
|
||||||
|
|
|
@ -19,7 +19,7 @@ use rustc_hash::FxHashSet;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use vfs::AbsPathBuf;
|
use vfs::AbsPathBuf;
|
||||||
|
|
||||||
use crate::diagnostics::DiagnosticsMapConfig;
|
use crate::{caps::enabled_completions_resolve_capabilities, diagnostics::DiagnosticsMapConfig};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -182,7 +182,7 @@ impl Config {
|
||||||
},
|
},
|
||||||
completion: CompletionConfig {
|
completion: CompletionConfig {
|
||||||
enable_postfix_completions: true,
|
enable_postfix_completions: true,
|
||||||
enable_experimental_completions: true,
|
enable_autoimport_completions: true,
|
||||||
add_call_parenthesis: true,
|
add_call_parenthesis: true,
|
||||||
add_call_argument_snippets: true,
|
add_call_argument_snippets: true,
|
||||||
..CompletionConfig::default()
|
..CompletionConfig::default()
|
||||||
|
@ -305,7 +305,7 @@ impl Config {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.completion.enable_postfix_completions = data.completion_postfix_enable;
|
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_parenthesis = data.completion_addCallParenthesis;
|
||||||
self.completion.add_call_argument_snippets = data.completion_addCallArgumentSnippets;
|
self.completion.add_call_argument_snippets = data.completion_addCallArgumentSnippets;
|
||||||
self.completion.merge = self.assist.insert_use.merge;
|
self.completion.merge = self.assist.insert_use.merge;
|
||||||
|
@ -388,6 +388,8 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.completion.allow_snippets(false);
|
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) = &doc_caps.completion {
|
||||||
if let Some(completion_item) = &completion.completion_item {
|
if let Some(completion_item) = &completion.completion_item {
|
||||||
if let Some(value) = completion_item.snippet_support {
|
if let Some(value) = completion_item.snippet_support {
|
||||||
|
@ -506,7 +508,7 @@ config_data! {
|
||||||
completion_addCallArgumentSnippets: bool = true,
|
completion_addCallArgumentSnippets: bool = true,
|
||||||
completion_addCallParenthesis: bool = true,
|
completion_addCallParenthesis: bool = true,
|
||||||
completion_postfix_enable: bool = true,
|
completion_postfix_enable: bool = true,
|
||||||
completion_enableExperimental: bool = true,
|
completion_autoimport_enable: bool = true,
|
||||||
|
|
||||||
diagnostics_enable: bool = true,
|
diagnostics_enable: bool = true,
|
||||||
diagnostics_enableExperimental: bool = true,
|
diagnostics_enableExperimental: bool = true,
|
||||||
|
|
|
@ -8,8 +8,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use ide::{
|
use ide::{
|
||||||
FileId, FilePosition, FileRange, HoverAction, HoverGotoTypeData, NavigationTarget, Query,
|
CompletionResolveCapability, FileId, FilePosition, FileRange, HoverAction, HoverGotoTypeData,
|
||||||
RangeInfo, Runnable, RunnableKind, SearchScope, TextEdit,
|
NavigationTarget, Query, RangeInfo, Runnable, RunnableKind, SearchScope, TextEdit,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lsp_server::ErrorCode;
|
use lsp_server::ErrorCode;
|
||||||
|
@ -21,7 +21,7 @@ use lsp_types::{
|
||||||
HoverContents, Location, NumberOrString, Position, PrepareRenameResponse, Range, RenameParams,
|
HoverContents, Location, NumberOrString, Position, PrepareRenameResponse, Range, RenameParams,
|
||||||
SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, SemanticTokensParams,
|
SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, SemanticTokensParams,
|
||||||
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
|
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation,
|
||||||
SymbolTag, TextDocumentIdentifier, Url, WorkspaceEdit,
|
SymbolTag, TextDocumentIdentifier, TextDocumentPositionParams, Url, WorkspaceEdit,
|
||||||
};
|
};
|
||||||
use project_model::TargetKind;
|
use project_model::TargetKind;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -35,6 +35,7 @@ use crate::{
|
||||||
from_json, from_proto,
|
from_json, from_proto,
|
||||||
global_state::{GlobalState, GlobalStateSnapshot},
|
global_state::{GlobalState, GlobalStateSnapshot},
|
||||||
lsp_ext::{self, InlayHint, InlayHintsParams},
|
lsp_ext::{self, InlayHint, InlayHintsParams},
|
||||||
|
lsp_utils::all_edits_are_disjoint,
|
||||||
to_proto, LspError, Result,
|
to_proto, LspError, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -539,6 +540,7 @@ pub(crate) fn handle_completion(
|
||||||
params: lsp_types::CompletionParams,
|
params: lsp_types::CompletionParams,
|
||||||
) -> Result<Option<lsp_types::CompletionResponse>> {
|
) -> Result<Option<lsp_types::CompletionResponse>> {
|
||||||
let _p = profile::span("handle_completion");
|
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 position = from_proto::file_position(&snap, params.text_document_position)?;
|
||||||
let completion_triggered_after_single_colon = {
|
let completion_triggered_after_single_colon = {
|
||||||
let mut res = false;
|
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_index = snap.analysis.file_line_index(position.file_id)?;
|
||||||
let line_endings = snap.file_line_endings(position.file_id);
|
let line_endings = snap.file_line_endings(position.file_id);
|
||||||
|
|
||||||
let items: Vec<CompletionItem> = items
|
let items: Vec<CompletionItem> = items
|
||||||
.into_iter()
|
.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();
|
.collect();
|
||||||
|
|
||||||
let completion_list = lsp_types::CompletionList { is_incomplete: true, items };
|
let completion_list = lsp_types::CompletionList { is_incomplete: true, items };
|
||||||
Ok(Some(completion_list.into()))
|
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(
|
pub(crate) fn handle_folding_range(
|
||||||
snap: GlobalStateSnapshot,
|
snap: GlobalStateSnapshot,
|
||||||
params: FoldingRangeParams,
|
params: FoldingRangeParams,
|
||||||
|
@ -1534,3 +1620,30 @@ fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>)
|
||||||
_ => false,
|
_ => 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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use lsp_types::{Position, Range, TextDocumentContentChangeEvent};
|
use lsp_types::{
|
||||||
|
CompletionItem, CompletionTextEdit, InsertReplaceEdit, Position, Range,
|
||||||
|
TextDocumentContentChangeEvent,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -197,4 +228,135 @@ mod tests {
|
||||||
apply_document_changes(&mut text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]);
|
apply_document_changes(&mut text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]);
|
||||||
assert_eq!(text, "ațc\ncb");
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,6 +454,7 @@ impl GlobalState {
|
||||||
.on::<lsp_types::request::GotoImplementation>(handlers::handle_goto_implementation)
|
.on::<lsp_types::request::GotoImplementation>(handlers::handle_goto_implementation)
|
||||||
.on::<lsp_types::request::GotoTypeDefinition>(handlers::handle_goto_type_definition)
|
.on::<lsp_types::request::GotoTypeDefinition>(handlers::handle_goto_type_definition)
|
||||||
.on::<lsp_types::request::Completion>(handlers::handle_completion)
|
.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::CodeLensRequest>(handlers::handle_code_lens)
|
||||||
.on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)
|
.on::<lsp_types::request::CodeLensResolve>(handlers::handle_code_lens_resolve)
|
||||||
.on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)
|
.on::<lsp_types::request::FoldingRangeRequest>(handlers::handle_folding_range)
|
||||||
|
|
|
@ -460,10 +460,13 @@
|
||||||
"default": true,
|
"default": true,
|
||||||
"markdownDescription": "Whether to show postfix snippets like `dbg`, `if`, `not`, etc."
|
"markdownDescription": "Whether to show postfix snippets like `dbg`, `if`, `not`, etc."
|
||||||
},
|
},
|
||||||
"rust-analyzer.completion.enableExperimental": {
|
"rust-analyzer.completion.autoimport.enable": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"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": {
|
"rust-analyzer.callInfo.full": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|
Loading…
Reference in a new issue