Skip to content

Commit 4d4f119

Browse files
Merge #6706
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 <[email protected]>
2 parents 021e97e + bf24cb3 commit 4d4f119

File tree

17 files changed

+565
-118
lines changed

17 files changed

+565
-118
lines changed

crates/completion/src/completions/unqualified_path.rs

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use test_utils::mark;
99

1010
use crate::{
1111
render::{render_resolution_with_import, RenderContext},
12-
CompletionContext, Completions,
12+
CompletionContext, Completions, ImportEdit,
1313
};
1414

1515
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
4444
acc.add_resolution(ctx, name.to_string(), &res)
4545
});
4646

47-
if ctx.config.enable_experimental_completions {
47+
if ctx.config.enable_autoimport_completions && ctx.config.resolve_additional_edits_lazily() {
4848
fuzzy_completion(acc, ctx).unwrap_or_default()
4949
}
5050
}
@@ -73,19 +73,64 @@ fn complete_enum_variants(acc: &mut Completions, ctx: &CompletionContext, ty: &T
7373
}
7474
}
7575

76+
// Feature: Fuzzy Completion and Autoimports
77+
//
78+
// When completing names in the current scope, proposes additional imports from other modules or crates,
79+
// if they can be qualified in the scope and their name contains all symbols from the completion input
80+
// (case-insensitive, in any order or places).
81+
//
82+
// ```
83+
// fn main() {
84+
// pda<|>
85+
// }
86+
// # pub mod std { pub mod marker { pub struct PhantomData { } } }
87+
// ```
88+
// ->
89+
// ```
90+
// use std::marker::PhantomData;
91+
//
92+
// fn main() {
93+
// PhantomData
94+
// }
95+
// # pub mod std { pub mod marker { pub struct PhantomData { } } }
96+
// ```
97+
//
98+
// .Fuzzy search details
99+
//
100+
// To avoid an excessive amount of the results returned, completion input is checked for inclusion in the identifiers only
101+
// (i.e. in `HashMap` in the `std::collections::HashMap` path), also not in the module indentifiers.
102+
//
103+
// .Merge Behaviour
104+
//
105+
// It is possible to configure how use-trees are merged with the `importMergeBehaviour` setting.
106+
// Mimics the corresponding behaviour of the `Auto Import` feature.
107+
//
108+
// .LSP and performance implications
109+
//
110+
// The feature is enabled only if the LSP client supports LSP protocol version 3.16+ and reports the `additionalTextEdits`
111+
// (case sensitive) resolve client capability in its client capabilities.
112+
// This way the server is able to defer the costly computations, doing them for a selected completion item only.
113+
// For clients with no such support, all edits have to be calculated on the completion request, including the fuzzy search completion ones,
114+
// which might be slow ergo the feature is automatically disabled.
115+
//
116+
// .Feature toggle
117+
//
118+
// The feature can be forcefully turned off in the settings with the `rust-analyzer.completion.enableAutoimportCompletions` flag.
119+
// Note that having this flag set to `true` does not guarantee that the feature is enabled: your client needs to have the corredponding
120+
// capability enabled.
76121
fn fuzzy_completion(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
77122
let _p = profile::span("fuzzy_completion");
123+
let potential_import_name = ctx.token.to_string();
124+
78125
let current_module = ctx.scope.module()?;
79126
let anchor = ctx.name_ref_syntax.as_ref()?;
80127
let import_scope = ImportScope::find_insert_use_container(anchor.syntax(), &ctx.sema)?;
81128

82-
let potential_import_name = ctx.token.to_string();
83-
84129
let possible_imports = imports_locator::find_similar_imports(
85130
&ctx.sema,
86131
ctx.krate?,
132+
Some(100),
87133
&potential_import_name,
88-
50,
89134
true,
90135
)
91136
.filter_map(|import_candidate| {
@@ -99,13 +144,14 @@ fn fuzzy_completion(acc: &mut Completions, ctx: &CompletionContext) -> Option<()
99144
})
100145
})
101146
.filter(|(mod_path, _)| mod_path.len() > 1)
102-
.take(20)
103147
.filter_map(|(import_path, definition)| {
104148
render_resolution_with_import(
105149
RenderContext::new(ctx),
106-
import_path.clone(),
107-
import_scope.clone(),
108-
ctx.config.merge,
150+
ImportEdit {
151+
import_path: import_path.clone(),
152+
import_scope: import_scope.clone(),
153+
merge_behaviour: ctx.config.merge,
154+
},
109155
&definition,
110156
)
111157
});
@@ -120,8 +166,8 @@ mod tests {
120166
use test_utils::mark;
121167

122168
use crate::{
123-
test_utils::{check_edit, completion_list},
124-
CompletionKind,
169+
test_utils::{check_edit, check_edit_with_config, completion_list},
170+
CompletionConfig, CompletionKind,
125171
};
126172

127173
fn check(ra_fixture: &str, expect: Expect) {
@@ -730,7 +776,13 @@ impl My<|>
730776

731777
#[test]
732778
fn function_fuzzy_completion() {
733-
check_edit(
779+
let mut completion_config = CompletionConfig::default();
780+
completion_config
781+
.active_resolve_capabilities
782+
.insert(crate::CompletionResolveCapability::AdditionalTextEdits);
783+
784+
check_edit_with_config(
785+
completion_config,
734786
"stdin",
735787
r#"
736788
//- /lib.rs crate:dep
@@ -755,7 +807,13 @@ fn main() {
755807

756808
#[test]
757809
fn macro_fuzzy_completion() {
758-
check_edit(
810+
let mut completion_config = CompletionConfig::default();
811+
completion_config
812+
.active_resolve_capabilities
813+
.insert(crate::CompletionResolveCapability::AdditionalTextEdits);
814+
815+
check_edit_with_config(
816+
completion_config,
759817
"macro_with_curlies!",
760818
r#"
761819
//- /lib.rs crate:dep
@@ -782,7 +840,13 @@ fn main() {
782840

783841
#[test]
784842
fn struct_fuzzy_completion() {
785-
check_edit(
843+
let mut completion_config = CompletionConfig::default();
844+
completion_config
845+
.active_resolve_capabilities
846+
.insert(crate::CompletionResolveCapability::AdditionalTextEdits);
847+
848+
check_edit_with_config(
849+
completion_config,
786850
"ThirdStruct",
787851
r#"
788852
//- /lib.rs crate:dep

crates/completion/src/config.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,42 @@
55
//! completions if we are allowed to.
66
77
use ide_db::helpers::insert_use::MergeBehaviour;
8+
use rustc_hash::FxHashSet;
89

910
#[derive(Clone, Debug, PartialEq, Eq)]
1011
pub struct CompletionConfig {
1112
pub enable_postfix_completions: bool,
12-
pub enable_experimental_completions: bool,
13+
pub enable_autoimport_completions: bool,
1314
pub add_call_parenthesis: bool,
1415
pub add_call_argument_snippets: bool,
1516
pub snippet_cap: Option<SnippetCap>,
1617
pub merge: Option<MergeBehaviour>,
18+
/// A set of capabilities, enabled on the client and supported on the server.
19+
pub active_resolve_capabilities: FxHashSet<CompletionResolveCapability>,
20+
}
21+
22+
/// A resolve capability, supported on the server.
23+
/// If the client registers any completion resolve capabilities,
24+
/// the server is able to render completion items' corresponding fields later,
25+
/// not during an initial completion item request.
26+
/// See https://github.com/rust-analyzer/rust-analyzer/issues/6366 for more details.
27+
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
28+
pub enum CompletionResolveCapability {
29+
Documentation,
30+
Detail,
31+
AdditionalTextEdits,
1732
}
1833

1934
impl CompletionConfig {
2035
pub fn allow_snippets(&mut self, yes: bool) {
2136
self.snippet_cap = if yes { Some(SnippetCap { _private: () }) } else { None }
2237
}
38+
39+
/// Whether the completions' additional edits are calculated when sending an initional completions list
40+
/// or later, in a separate resolve request.
41+
pub fn resolve_additional_edits_lazily(&self) -> bool {
42+
self.active_resolve_capabilities.contains(&CompletionResolveCapability::AdditionalTextEdits)
43+
}
2344
}
2445

2546
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -31,11 +52,12 @@ impl Default for CompletionConfig {
3152
fn default() -> Self {
3253
CompletionConfig {
3354
enable_postfix_completions: true,
34-
enable_experimental_completions: true,
55+
enable_autoimport_completions: true,
3556
add_call_parenthesis: true,
3657
add_call_argument_snippets: true,
3758
snippet_cap: Some(SnippetCap { _private: () }),
3859
merge: Some(MergeBehaviour::Full),
60+
active_resolve_capabilities: FxHashSet::default(),
3961
}
4062
}
4163
}

crates/completion/src/item.rs

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::config::SnippetCap;
1515
/// `CompletionItem` describes a single completion variant in the editor pop-up.
1616
/// It is basically a POD with various properties. To construct a
1717
/// `CompletionItem`, use `new` method and the `Builder` struct.
18+
#[derive(Clone)]
1819
pub struct CompletionItem {
1920
/// Used only internally in tests, to check only specific kind of
2021
/// completion (postfix, keyword, reference, etc).
@@ -65,6 +66,9 @@ pub struct CompletionItem {
6566
/// Indicates that a reference or mutable reference to this variable is a
6667
/// possible match.
6768
ref_match: Option<(Mutability, CompletionScore)>,
69+
70+
/// The import data to add to completion's edits.
71+
import_to_add: Option<ImportEdit>,
6872
}
6973

7074
// We use custom debug for CompletionItem to make snapshot tests more readable.
@@ -256,14 +260,37 @@ impl CompletionItem {
256260
pub fn ref_match(&self) -> Option<(Mutability, CompletionScore)> {
257261
self.ref_match
258262
}
263+
264+
pub fn import_to_add(&self) -> Option<&ImportEdit> {
265+
self.import_to_add.as_ref()
266+
}
259267
}
260268

261269
/// An extra import to add after the completion is applied.
262-
#[derive(Clone)]
263-
pub(crate) struct ImportToAdd {
264-
pub(crate) import_path: ModPath,
265-
pub(crate) import_scope: ImportScope,
266-
pub(crate) merge_behaviour: Option<MergeBehaviour>,
270+
#[derive(Debug, Clone)]
271+
pub struct ImportEdit {
272+
pub import_path: ModPath,
273+
pub import_scope: ImportScope,
274+
pub merge_behaviour: Option<MergeBehaviour>,
275+
}
276+
277+
impl ImportEdit {
278+
/// Attempts to insert the import to the given scope, producing a text edit.
279+
/// May return no edit in edge cases, such as scope already containing the import.
280+
pub fn to_text_edit(&self) -> Option<TextEdit> {
281+
let _p = profile::span("ImportEdit::to_text_edit");
282+
283+
let rewriter = insert_use::insert_use(
284+
&self.import_scope,
285+
mod_path_to_ast(&self.import_path),
286+
self.merge_behaviour,
287+
);
288+
let old_ast = rewriter.rewrite_root()?;
289+
let mut import_insert = TextEdit::builder();
290+
algo::diff(&old_ast, &rewriter.rewrite(&old_ast)).into_text_edit(&mut import_insert);
291+
292+
Some(import_insert.finish())
293+
}
267294
}
268295

269296
/// A helper to make `CompletionItem`s.
@@ -272,7 +299,7 @@ pub(crate) struct ImportToAdd {
272299
pub(crate) struct Builder {
273300
source_range: TextRange,
274301
completion_kind: CompletionKind,
275-
import_to_add: Option<ImportToAdd>,
302+
import_to_add: Option<ImportEdit>,
276303
label: String,
277304
insert_text: Option<String>,
278305
insert_text_format: InsertTextFormat,
@@ -294,11 +321,9 @@ impl Builder {
294321
let mut label = self.label;
295322
let mut lookup = self.lookup;
296323
let mut insert_text = self.insert_text;
297-
let mut text_edits = TextEdit::builder();
298324

299-
if let Some(import_data) = self.import_to_add {
300-
let import = mod_path_to_ast(&import_data.import_path);
301-
let mut import_path_without_last_segment = import_data.import_path;
325+
if let Some(import_to_add) = self.import_to_add.as_ref() {
326+
let mut import_path_without_last_segment = import_to_add.import_path.to_owned();
302327
let _ = import_path_without_last_segment.segments.pop();
303328

304329
if !import_path_without_last_segment.segments.is_empty() {
@@ -310,32 +335,20 @@ impl Builder {
310335
}
311336
label = format!("{}::{}", import_path_without_last_segment, label);
312337
}
313-
314-
let rewriter = insert_use::insert_use(
315-
&import_data.import_scope,
316-
import,
317-
import_data.merge_behaviour,
318-
);
319-
if let Some(old_ast) = rewriter.rewrite_root() {
320-
algo::diff(&old_ast, &rewriter.rewrite(&old_ast)).into_text_edit(&mut text_edits);
321-
}
322338
}
323339

324-
let original_edit = match self.text_edit {
340+
let text_edit = match self.text_edit {
325341
Some(it) => it,
326342
None => {
327343
TextEdit::replace(self.source_range, insert_text.unwrap_or_else(|| label.clone()))
328344
}
329345
};
330346

331-
let mut resulting_edit = text_edits.finish();
332-
resulting_edit.union(original_edit).expect("Failed to unite text edits");
333-
334347
CompletionItem {
335348
source_range: self.source_range,
336349
label,
337350
insert_text_format: self.insert_text_format,
338-
text_edit: resulting_edit,
351+
text_edit,
339352
detail: self.detail,
340353
documentation: self.documentation,
341354
lookup,
@@ -345,6 +358,7 @@ impl Builder {
345358
trigger_call_info: self.trigger_call_info.unwrap_or(false),
346359
score: self.score,
347360
ref_match: self.ref_match,
361+
import_to_add: self.import_to_add,
348362
}
349363
}
350364
pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder {
@@ -407,7 +421,7 @@ impl Builder {
407421
self.trigger_call_info = Some(true);
408422
self
409423
}
410-
pub(crate) fn add_import(mut self, import_to_add: Option<ImportToAdd>) -> Builder {
424+
pub(crate) fn add_import(mut self, import_to_add: Option<ImportEdit>) -> Builder {
411425
self.import_to_add = import_to_add;
412426
self
413427
}

0 commit comments

Comments
 (0)