Skip to content

Commit

Permalink
Merge #6706
Browse files Browse the repository at this point in the history
6706: Move import text edit calculation into a completion resolve request r=matklad a=SomeoneToIgnore

Part of #6612 (presumably fixing it)
Part of #6366 (does not cover all possible resolve capabilities we can do)
Closes #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: #6633 (comment)

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
  • Loading branch information
bors[bot] and SomeoneToIgnore authored Dec 8, 2020
2 parents 021e97e + bf24cb3 commit 4d4f119
Show file tree
Hide file tree
Showing 17 changed files with 565 additions and 118 deletions.
92 changes: 78 additions & 14 deletions crates/completion/src/completions/unqualified_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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| {
Expand All @@ -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,
)
});
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
26 changes: 24 additions & 2 deletions crates/completion/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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(),
}
}
}
64 changes: 39 additions & 25 deletions crates/completion/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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() {
Expand All @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 4d4f119

Please sign in to comment.