aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-03-12 14:23:32 +0000
committerGitHub <[email protected]>2021-03-12 14:23:32 +0000
commit19dd1fd4d41538de7ea386a2d0d18e27bf95f63c (patch)
tree8f7d8057858f366060b6e71c027a3053269a1c8d
parent393e2356594560dec84e73471f26a366a6fc91dd (diff)
parentacbe297fbd10250e0d99d1e3e98751dd6ef77adc (diff)
Merge #7904
7904: Improved completion sorting r=JoshMcguigan a=JoshMcguigan I was working on extending #3954 to apply completion scores in more places (I'll have another PR open for that soon) when I discovered that actually completion sorting was not working for me at all in `coc.nvim`. This led me down a bit of a rabbit hole of how coc and vs code each sort completion items. Before this PR, rust-analyzer was setting the `sortText` field on completion items to `None` if we hadn't applied any completion score for that item, or to the label of the item with a leading whitespace character if we had applied any completion score. Completion score is defined in rust-analyzer as an enum with two variants, `TypeMatch` and `TypeAndNameMatch`. In vs code the above strategy works, because if `sortText` isn't set [they default it to the label](https://github.com/microsoft/vscode/commit/b4ead4ed665e1ce2e58d8329c682f78da2d4e89d). However, coc [does not do this](https://github.com/neoclide/coc.nvim/blob/e211e361475a38b146a903b9b02343551c6cd372/src/completion/complete.ts#L245). I was going to file a bug report against coc, but I read the [LSP spec for the `sortText` field](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion) and I feel like it is ambiguous and coc could claim what they do is a valid interpretation of the spec. Further, the existing rust-analyzer behavior of prepending a leading whitespace character for completion items with any completion score does not handle sorting `TypeAndNameMatch` completions above `TypeMatch` completions. They were both being treated the same. The first change this PR makes is to set the `sortText` field to either "1" for `TypeAndNameMatch` completions, "2" for `TypeMatch` completions, or "3" for completions which are neither of those. This change works around the potential ambiguity in the LSP spec and fixes completion sorting for users of coc. It also allows `TypeAndNameMatch` items to be sorted above just `TypeMatch` items (of course both of these will be sorted above completion items without a score). The second change this PR makes is to use the actual completion scores for ref matches. The existing code ignored the actual score and always assumed these would be a high priority completion item. #### Before Here coc just sorts based on how close the items are in the file. ![image](https://user-images.githubusercontent.com/22216761/110249880-46063580-7f2d-11eb-9233-91a2bbd48238.png) #### After Here we correctly get `zzz` first, since that is both a type and name match. Then we get `ccc` which is just a type match. ![image](https://user-images.githubusercontent.com/22216761/110249883-4e5e7080-7f2d-11eb-9269-a3bc133fdee7.png) Co-authored-by: Josh Mcguigan <[email protected]>
-rw-r--r--crates/ide/src/lib.rs2
-rw-r--r--crates/ide_completion/src/item.rs123
-rw-r--r--crates/ide_completion/src/lib.rs5
-rw-r--r--crates/ide_completion/src/render.rs22
-rw-r--r--crates/rust-analyzer/src/to_proto.rs34
5 files changed, 140 insertions, 46 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index d1a250d48..0a493d2f3 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -87,7 +87,7 @@ pub use crate::{
87pub use hir::{Documentation, Semantics}; 87pub use hir::{Documentation, Semantics};
88pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind}; 88pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind};
89pub use ide_completion::{ 89pub use ide_completion::{
90 CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, 90 CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit,
91 InsertTextFormat, 91 InsertTextFormat,
92}; 92};
93pub use ide_db::{ 93pub use ide_db::{
diff --git a/crates/ide_completion/src/item.rs b/crates/ide_completion/src/item.rs
index cf1aaa131..3febab32b 100644
--- a/crates/ide_completion/src/item.rs
+++ b/crates/ide_completion/src/item.rs
@@ -70,7 +70,7 @@ pub struct CompletionItem {
70 /// Note that Relevance ignores fuzzy match score. We compute Relevance for 70 /// Note that Relevance ignores fuzzy match score. We compute Relevance for
71 /// all possible items, and then separately build an ordered completion list 71 /// all possible items, and then separately build an ordered completion list
72 /// based on relevance and fuzzy matching with the already typed identifier. 72 /// based on relevance and fuzzy matching with the already typed identifier.
73 relevance: Relevance, 73 relevance: CompletionRelevance,
74 74
75 /// Indicates that a reference or mutable reference to this variable is a 75 /// Indicates that a reference or mutable reference to this variable is a
76 /// possible match. 76 /// possible match.
@@ -107,9 +107,11 @@ impl fmt::Debug for CompletionItem {
107 if self.deprecated { 107 if self.deprecated {
108 s.field("deprecated", &true); 108 s.field("deprecated", &true);
109 } 109 }
110 if self.relevance.is_relevant() { 110
111 if self.relevance != CompletionRelevance::default() {
111 s.field("relevance", &self.relevance); 112 s.field("relevance", &self.relevance);
112 } 113 }
114
113 if let Some(mutability) = &self.ref_match { 115 if let Some(mutability) = &self.ref_match {
114 s.field("ref_match", &format!("&{}", mutability.as_keyword_for_ref())); 116 s.field("ref_match", &format!("&{}", mutability.as_keyword_for_ref()));
115 } 117 }
@@ -120,16 +122,8 @@ impl fmt::Debug for CompletionItem {
120 } 122 }
121} 123}
122 124
123#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
124pub enum CompletionScore {
125 /// If only type match
126 TypeMatch,
127 /// If type and name match
128 TypeAndNameMatch,
129}
130
131#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Default)] 125#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Default)]
132pub struct Relevance { 126pub struct CompletionRelevance {
133 /// This is set in cases like these: 127 /// This is set in cases like these:
134 /// 128 ///
135 /// ``` 129 /// ```
@@ -152,9 +146,34 @@ pub struct Relevance {
152 pub exact_type_match: bool, 146 pub exact_type_match: bool,
153} 147}
154 148
155impl Relevance { 149impl CompletionRelevance {
150 /// Provides a relevance score. Higher values are more relevant.
151 ///
152 /// The absolute value of the relevance score is not meaningful, for
153 /// example a value of 0 doesn't mean "not relevant", rather
154 /// it means "least relevant". The score value should only be used
155 /// for relative ordering.
156 ///
157 /// See is_relevant if you need to make some judgement about score
158 /// in an absolute sense.
159 pub fn score(&self) -> u32 {
160 let mut score = 0;
161
162 if self.exact_name_match {
163 score += 1;
164 }
165 if self.exact_type_match {
166 score += 1;
167 }
168
169 score
170 }
171
172 /// Returns true when the score for this threshold is above
173 /// some threshold such that we think it is especially likely
174 /// to be relevant.
156 pub fn is_relevant(&self) -> bool { 175 pub fn is_relevant(&self) -> bool {
157 self != &Relevance::default() 176 self.score() > 0
158 } 177 }
159} 178}
160 179
@@ -249,7 +268,7 @@ impl CompletionItem {
249 text_edit: None, 268 text_edit: None,
250 deprecated: false, 269 deprecated: false,
251 trigger_call_info: None, 270 trigger_call_info: None,
252 relevance: Relevance::default(), 271 relevance: CompletionRelevance::default(),
253 ref_match: None, 272 ref_match: None,
254 import_to_add: None, 273 import_to_add: None,
255 } 274 }
@@ -292,7 +311,7 @@ impl CompletionItem {
292 self.deprecated 311 self.deprecated
293 } 312 }
294 313
295 pub fn relevance(&self) -> Relevance { 314 pub fn relevance(&self) -> CompletionRelevance {
296 self.relevance 315 self.relevance
297 } 316 }
298 317
@@ -300,8 +319,14 @@ impl CompletionItem {
300 self.trigger_call_info 319 self.trigger_call_info
301 } 320 }
302 321
303 pub fn ref_match(&self) -> Option<Mutability> { 322 pub fn ref_match(&self) -> Option<(Mutability, CompletionRelevance)> {
304 self.ref_match 323 // Relevance of the ref match should be the same as the original
324 // match, but with exact type match set because self.ref_match
325 // is only set if there is an exact type match.
326 let mut relevance = self.relevance;
327 relevance.exact_type_match = true;
328
329 self.ref_match.map(|mutability| (mutability, relevance))
305 } 330 }
306 331
307 pub fn import_to_add(&self) -> Option<&ImportEdit> { 332 pub fn import_to_add(&self) -> Option<&ImportEdit> {
@@ -349,7 +374,7 @@ pub(crate) struct Builder {
349 text_edit: Option<TextEdit>, 374 text_edit: Option<TextEdit>,
350 deprecated: bool, 375 deprecated: bool,
351 trigger_call_info: Option<bool>, 376 trigger_call_info: Option<bool>,
352 relevance: Relevance, 377 relevance: CompletionRelevance,
353 ref_match: Option<Mutability>, 378 ref_match: Option<Mutability>,
354} 379}
355 380
@@ -457,7 +482,7 @@ impl Builder {
457 self.deprecated = deprecated; 482 self.deprecated = deprecated;
458 self 483 self
459 } 484 }
460 pub(crate) fn set_relevance(&mut self, relevance: Relevance) -> &mut Builder { 485 pub(crate) fn set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder {
461 self.relevance = relevance; 486 self.relevance = relevance;
462 self 487 self
463 } 488 }
@@ -474,3 +499,63 @@ impl Builder {
474 self 499 self
475 } 500 }
476} 501}
502
503#[cfg(test)]
504mod tests {
505 use itertools::Itertools;
506 use test_utils::assert_eq_text;
507
508 use super::CompletionRelevance;
509
510 /// Check that these are CompletionRelevance are sorted in ascending order
511 /// by their relevance score.
512 ///
513 /// We want to avoid making assertions about the absolute score of any
514 /// item, but we do want to assert whether each is >, <, or == to the
515 /// others.
516 ///
517 /// If provided vec![vec![a], vec![b, c], vec![d]], then this will assert:
518 /// a.score < b.score == c.score < d.score
519 fn check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>) {
520 let expected = format!("{:#?}", &expected_relevance_order);
521
522 let actual_relevance_order = expected_relevance_order
523 .into_iter()
524 .flatten()
525 .map(|r| (r.score(), r))
526 .sorted_by_key(|(score, _r)| *score)
527 .fold(
528 (u32::MIN, vec![vec![]]),
529 |(mut currently_collecting_score, mut out), (score, r)| {
530 if currently_collecting_score == score {
531 out.last_mut().unwrap().push(r);
532 } else {
533 currently_collecting_score = score;
534 out.push(vec![r]);
535 }
536 (currently_collecting_score, out)
537 },
538 )
539 .1;
540
541 let actual = format!("{:#?}", &actual_relevance_order);
542
543 assert_eq_text!(&expected, &actual);
544 }
545
546 #[test]
547 fn relevance_score() {
548 // This test asserts that the relevance score for these items is ascending, and
549 // that any items in the same vec have the same score.
550 let expected_relevance_order = vec![
551 vec![CompletionRelevance::default()],
552 vec![
553 CompletionRelevance { exact_name_match: true, ..CompletionRelevance::default() },
554 CompletionRelevance { exact_type_match: true, ..CompletionRelevance::default() },
555 ],
556 vec![CompletionRelevance { exact_name_match: true, exact_type_match: true }],
557 ];
558
559 check_relevance_score_ordered(expected_relevance_order);
560 }
561}
diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs
index d46f521a0..263554ecf 100644
--- a/crates/ide_completion/src/lib.rs
+++ b/crates/ide_completion/src/lib.rs
@@ -23,10 +23,7 @@ use crate::{completions::Completions, context::CompletionContext, item::Completi
23 23
24pub use crate::{ 24pub use crate::{
25 config::CompletionConfig, 25 config::CompletionConfig,
26 item::{ 26 item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, InsertTextFormat},
27 CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, InsertTextFormat,
28 Relevance,
29 },
30}; 27};
31 28
32//FIXME: split the following feature into fine-grained features. 29//FIXME: split the following feature into fine-grained features.
diff --git a/crates/ide_completion/src/render.rs b/crates/ide_completion/src/render.rs
index f7f9084d9..db31896e5 100644
--- a/crates/ide_completion/src/render.rs
+++ b/crates/ide_completion/src/render.rs
@@ -20,7 +20,7 @@ use ide_db::{
20use syntax::TextRange; 20use syntax::TextRange;
21 21
22use crate::{ 22use crate::{
23 item::{ImportEdit, Relevance}, 23 item::{CompletionRelevance, ImportEdit},
24 CompletionContext, CompletionItem, CompletionItemKind, CompletionKind, 24 CompletionContext, CompletionItem, CompletionItemKind, CompletionKind,
25}; 25};
26 26
@@ -322,9 +322,9 @@ impl<'a> Render<'a> {
322 } 322 }
323} 323}
324 324
325fn compute_relevance(ctx: &RenderContext, ty: &Type, name: &str) -> Option<Relevance> { 325fn compute_relevance(ctx: &RenderContext, ty: &Type, name: &str) -> Option<CompletionRelevance> {
326 let (expected_name, expected_type) = ctx.expected_name_and_type()?; 326 let (expected_name, expected_type) = ctx.expected_name_and_type()?;
327 let mut res = Relevance::default(); 327 let mut res = CompletionRelevance::default();
328 res.exact_type_match = ty == &expected_type; 328 res.exact_type_match = ty == &expected_type;
329 res.exact_name_match = name == &expected_name; 329 res.exact_name_match = name == &expected_name;
330 Some(res) 330 Some(res)
@@ -338,7 +338,7 @@ mod tests {
338 338
339 use crate::{ 339 use crate::{
340 test_utils::{check_edit, do_completion, get_all_items, TEST_CONFIG}, 340 test_utils::{check_edit, do_completion, get_all_items, TEST_CONFIG},
341 CompletionKind, Relevance, 341 CompletionKind, CompletionRelevance,
342 }; 342 };
343 343
344 fn check(ra_fixture: &str, expect: Expect) { 344 fn check(ra_fixture: &str, expect: Expect) {
@@ -347,12 +347,14 @@ mod tests {
347 } 347 }
348 348
349 fn check_relevance(ra_fixture: &str, expect: Expect) { 349 fn check_relevance(ra_fixture: &str, expect: Expect) {
350 fn display_relevance(relevance: Relevance) -> &'static str { 350 fn display_relevance(relevance: CompletionRelevance) -> &'static str {
351 match relevance { 351 match relevance {
352 Relevance { exact_type_match: true, exact_name_match: true } => "[type+name]", 352 CompletionRelevance { exact_type_match: true, exact_name_match: true } => {
353 Relevance { exact_type_match: true, exact_name_match: false } => "[type]", 353 "[type+name]"
354 Relevance { exact_type_match: false, exact_name_match: true } => "[name]", 354 }
355 Relevance { exact_type_match: false, exact_name_match: false } => "[]", 355 CompletionRelevance { exact_type_match: true, exact_name_match: false } => "[type]",
356 CompletionRelevance { exact_type_match: false, exact_name_match: true } => "[name]",
357 CompletionRelevance { exact_type_match: false, exact_name_match: false } => "[]",
356 } 358 }
357 } 359 }
358 360
@@ -975,7 +977,7 @@ fn main() {
975 Local, 977 Local,
976 ), 978 ),
977 detail: "S", 979 detail: "S",
978 relevance: Relevance { 980 relevance: CompletionRelevance {
979 exact_name_match: true, 981 exact_name_match: true,
980 exact_type_match: false, 982 exact_type_match: false,
981 }, 983 },
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index a9846fa70..1a8cdadad 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -6,9 +6,10 @@ use std::{
6 6
7use ide::{ 7use ide::{
8 Annotation, AnnotationKind, Assist, AssistKind, CallInfo, CompletionItem, CompletionItemKind, 8 Annotation, AnnotationKind, Assist, AssistKind, CallInfo, CompletionItem, CompletionItemKind,
9 Documentation, FileId, FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HlMod, HlPunct, 9 CompletionRelevance, Documentation, FileId, FileRange, FileSystemEdit, Fold, FoldKind,
10 HlRange, HlTag, Indel, InlayHint, InlayKind, InsertTextFormat, Markup, NavigationTarget, 10 Highlight, HlMod, HlPunct, HlRange, HlTag, Indel, InlayHint, InlayKind, InsertTextFormat,
11 ReferenceAccess, RenameError, Runnable, Severity, SourceChange, TextEdit, TextRange, TextSize, 11 Markup, NavigationTarget, ReferenceAccess, RenameError, Runnable, Severity, SourceChange,
12 TextEdit, TextRange, TextSize,
12}; 13};
13use ide_db::SymbolKind; 14use ide_db::SymbolKind;
14use itertools::Itertools; 15use itertools::Itertools;
@@ -213,12 +214,22 @@ pub(crate) fn completion_item(
213 ..Default::default() 214 ..Default::default()
214 }; 215 };
215 216
216 if item.relevance().is_relevant() { 217 fn set_score(res: &mut lsp_types::CompletionItem, relevance: CompletionRelevance) {
217 lsp_item.preselect = Some(true); 218 if relevance.is_relevant() {
218 // HACK: sort preselect items first 219 res.preselect = Some(true);
219 lsp_item.sort_text = Some(format!(" {}", item.label())); 220 }
221 // The relevance needs to be inverted to come up with a sort score
222 // because the client will sort ascending.
223 let sort_score = relevance.score() ^ 0xFF_FF_FF_FF;
224 // Zero pad the string to ensure values can be properly sorted
225 // by the client. Hex format is used because it is easier to
226 // visually compare very large values, which the sort text
227 // tends to be since it is the opposite of the score.
228 res.sort_text = Some(format!("{:08x}", sort_score));
220 } 229 }
221 230
231 set_score(&mut lsp_item, item.relevance());
232
222 if item.deprecated() { 233 if item.deprecated() {
223 lsp_item.tags = Some(vec![lsp_types::CompletionItemTag::Deprecated]) 234 lsp_item.tags = Some(vec![lsp_types::CompletionItemTag::Deprecated])
224 } 235 }
@@ -228,10 +239,9 @@ pub(crate) fn completion_item(
228 } 239 }
229 240
230 let mut res = match item.ref_match() { 241 let mut res = match item.ref_match() {
231 Some(mutability) => { 242 Some((mutability, relevance)) => {
232 let mut lsp_item_with_ref = lsp_item.clone(); 243 let mut lsp_item_with_ref = lsp_item.clone();
233 lsp_item.preselect = Some(true); 244 set_score(&mut lsp_item_with_ref, relevance);
234 lsp_item.sort_text = Some(format!(" {}", item.label()));
235 lsp_item_with_ref.label = 245 lsp_item_with_ref.label =
236 format!("&{}{}", mutability.as_keyword_for_ref(), lsp_item_with_ref.label); 246 format!("&{}{}", mutability.as_keyword_for_ref(), lsp_item_with_ref.label);
237 if let Some(lsp_types::CompletionTextEdit::Edit(it)) = &mut lsp_item_with_ref.text_edit 247 if let Some(lsp_types::CompletionTextEdit::Edit(it)) = &mut lsp_item_with_ref.text_edit
@@ -1107,13 +1117,13 @@ mod tests {
1107 ( 1117 (
1108 "&arg", 1118 "&arg",
1109 Some( 1119 Some(
1110 " arg", 1120 "fffffffd",
1111 ), 1121 ),
1112 ), 1122 ),
1113 ( 1123 (
1114 "arg", 1124 "arg",
1115 Some( 1125 Some(
1116 " arg", 1126 "fffffffe",
1117 ), 1127 ),
1118 ), 1128 ),
1119 ] 1129 ]