diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2021-03-12 14:23:32 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2021-03-12 14:23:32 +0000 |
commit | 19dd1fd4d41538de7ea386a2d0d18e27bf95f63c (patch) | |
tree | 8f7d8057858f366060b6e71c027a3053269a1c8d | |
parent | 393e2356594560dec84e73471f26a366a6fc91dd (diff) | |
parent | acbe297fbd10250e0d99d1e3e98751dd6ef77adc (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.rs | 2 | ||||
-rw-r--r-- | crates/ide_completion/src/item.rs | 123 | ||||
-rw-r--r-- | crates/ide_completion/src/lib.rs | 5 | ||||
-rw-r--r-- | crates/ide_completion/src/render.rs | 22 | ||||
-rw-r--r-- | crates/rust-analyzer/src/to_proto.rs | 34 |
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::{ | |||
87 | pub use hir::{Documentation, Semantics}; | 87 | pub use hir::{Documentation, Semantics}; |
88 | pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind}; | 88 | pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind}; |
89 | pub use ide_completion::{ | 89 | pub use ide_completion::{ |
90 | CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, | 90 | CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, |
91 | InsertTextFormat, | 91 | InsertTextFormat, |
92 | }; | 92 | }; |
93 | pub use ide_db::{ | 93 | pub 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)] | ||
124 | pub 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)] |
132 | pub struct Relevance { | 126 | pub 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 | ||
155 | impl Relevance { | 149 | impl 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)] | ||
504 | mod 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 | ||
24 | pub use crate::{ | 24 | pub 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::{ | |||
20 | use syntax::TextRange; | 20 | use syntax::TextRange; |
21 | 21 | ||
22 | use crate::{ | 22 | use 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 | ||
325 | fn compute_relevance(ctx: &RenderContext, ty: &Type, name: &str) -> Option<Relevance> { | 325 | fn 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 | ||
7 | use ide::{ | 7 | use 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 | }; |
13 | use ide_db::SymbolKind; | 14 | use ide_db::SymbolKind; |
14 | use itertools::Itertools; | 15 | use 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 | ] |