1 //! See `CompletionItem` structure. 2 3 use std::fmt; 4 5 use hir::{Documentation, Mutability}; 6 use ide_db::{imports::import_assets::LocatedImport, RootDatabase, SnippetCap, SymbolKind}; 7 use itertools::Itertools; 8 use smallvec::SmallVec; 9 use stdx::{impl_from, never}; 10 use syntax::{SmolStr, TextRange, TextSize}; 11 use text_edit::TextEdit; 12 13 use crate::{ 14 context::{CompletionContext, PathCompletionCtx}, 15 render::{render_path_resolution, RenderContext}, 16 }; 17 18 /// `CompletionItem` describes a single completion entity which expands to 1 or more entries in the 19 /// editor pop-up. It is basically a POD with various properties. To construct a 20 /// [`CompletionItem`], use [`Builder::new`] method and the [`Builder`] struct. 21 #[derive(Clone)] 22 #[non_exhaustive] 23 pub struct CompletionItem { 24 /// Label in the completion pop up which identifies completion. 25 pub label: SmolStr, 26 /// Range of identifier that is being completed. 27 /// 28 /// It should be used primarily for UI, but we also use this to convert 29 /// generic TextEdit into LSP's completion edit (see conv.rs). 30 /// 31 /// `source_range` must contain the completion offset. `text_edit` should 32 /// start with what `source_range` points to, or VSCode will filter out the 33 /// completion silently. 34 pub source_range: TextRange, 35 /// What happens when user selects this item. 36 /// 37 /// Typically, replaces `source_range` with new identifier. 38 pub text_edit: TextEdit, 39 pub is_snippet: bool, 40 41 /// What item (struct, function, etc) are we completing. 42 pub kind: CompletionItemKind, 43 44 /// Lookup is used to check if completion item indeed can complete current 45 /// ident. 46 /// 47 /// That is, in `foo.bar$0` lookup of `abracadabra` will be accepted (it 48 /// contains `bar` sub sequence), and `quux` will rejected. 49 pub lookup: SmolStr, 50 51 /// Additional info to show in the UI pop up. 52 pub detail: Option<String>, 53 pub documentation: Option<Documentation>, 54 55 /// Whether this item is marked as deprecated 56 pub deprecated: bool, 57 58 /// If completing a function call, ask the editor to show parameter popup 59 /// after completion. 60 pub trigger_call_info: bool, 61 62 /// We use this to sort completion. Relevance records facts like "do the 63 /// types align precisely?". We can't sort by relevances directly, they are 64 /// only partially ordered. 65 /// 66 /// Note that Relevance ignores fuzzy match score. We compute Relevance for 67 /// all possible items, and then separately build an ordered completion list 68 /// based on relevance and fuzzy matching with the already typed identifier. 69 pub relevance: CompletionRelevance, 70 71 /// Indicates that a reference or mutable reference to this variable is a 72 /// possible match. 73 // FIXME: We shouldn't expose Mutability here (that is HIR types at all), its fine for now though 74 // until we have more splitting completions in which case we should think about 75 // generalizing this. See https://github.com/rust-lang/rust-analyzer/issues/12571 76 pub ref_match: Option<(Mutability, TextSize)>, 77 78 /// The import data to add to completion's edits. 79 /// (ImportPath, LastSegment) 80 pub import_to_add: SmallVec<[(String, String); 1]>, 81 } 82 83 // We use custom debug for CompletionItem to make snapshot tests more readable. 84 impl fmt::Debug for CompletionItem { fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 86 let mut s = f.debug_struct("CompletionItem"); 87 s.field("label", &self.label).field("source_range", &self.source_range); 88 if self.text_edit.len() == 1 { 89 let atom = &self.text_edit.iter().next().unwrap(); 90 s.field("delete", &atom.delete); 91 s.field("insert", &atom.insert); 92 } else { 93 s.field("text_edit", &self.text_edit); 94 } 95 s.field("kind", &self.kind); 96 if self.lookup() != self.label { 97 s.field("lookup", &self.lookup()); 98 } 99 if let Some(detail) = &self.detail { 100 s.field("detail", &detail); 101 } 102 if let Some(documentation) = &self.documentation { 103 s.field("documentation", &documentation); 104 } 105 if self.deprecated { 106 s.field("deprecated", &true); 107 } 108 109 if self.relevance != CompletionRelevance::default() { 110 s.field("relevance", &self.relevance); 111 } 112 113 if let Some((mutability, offset)) = &self.ref_match { 114 s.field("ref_match", &format!("&{}@{offset:?}", mutability.as_keyword_for_ref())); 115 } 116 if self.trigger_call_info { 117 s.field("trigger_call_info", &true); 118 } 119 s.finish() 120 } 121 } 122 123 #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] 124 pub struct CompletionRelevance { 125 /// This is set in cases like these: 126 /// 127 /// ``` 128 /// fn f(spam: String) {} 129 /// fn main { 130 /// let spam = 92; 131 /// f($0) // name of local matches the name of param 132 /// } 133 /// ``` 134 pub exact_name_match: bool, 135 /// See CompletionRelevanceTypeMatch doc comments for cases where this is set. 136 pub type_match: Option<CompletionRelevanceTypeMatch>, 137 /// This is set in cases like these: 138 /// 139 /// ``` 140 /// fn foo(a: u32) { 141 /// let b = 0; 142 /// $0 // `a` and `b` are local 143 /// } 144 /// ``` 145 pub is_local: bool, 146 /// This is set when trait items are completed in an impl of that trait. 147 pub is_item_from_trait: bool, 148 /// This is set when an import is suggested whose name is already imported. 149 pub is_name_already_imported: bool, 150 /// This is set for completions that will insert a `use` item. 151 pub requires_import: bool, 152 /// Set for method completions of the `core::ops` and `core::cmp` family. 153 pub is_op_method: bool, 154 /// Set for item completions that are private but in the workspace. 155 pub is_private_editable: bool, 156 /// Set for postfix snippet item completions 157 pub postfix_match: Option<CompletionRelevancePostfixMatch>, 158 /// This is set for type inference results 159 pub is_definite: bool, 160 } 161 162 #[derive(Debug, Clone, Copy, Eq, PartialEq)] 163 pub enum CompletionRelevanceTypeMatch { 164 /// This is set in cases like these: 165 /// 166 /// ``` 167 /// enum Option<T> { Some(T), None } 168 /// fn f(a: Option<u32>) {} 169 /// fn main { 170 /// f(Option::N$0) // type `Option<T>` could unify with `Option<u32>` 171 /// } 172 /// ``` 173 CouldUnify, 174 /// This is set in cases like these: 175 /// 176 /// ``` 177 /// fn f(spam: String) {} 178 /// fn main { 179 /// let foo = String::new(); 180 /// f($0) // type of local matches the type of param 181 /// } 182 /// ``` 183 Exact, 184 } 185 186 #[derive(Debug, Clone, Copy, Eq, PartialEq)] 187 pub enum CompletionRelevancePostfixMatch { 188 /// Set in cases when item is postfix, but not exact 189 NonExact, 190 /// This is set in cases like these: 191 /// 192 /// ``` 193 /// (a > b).not$0 194 /// ``` 195 /// 196 /// Basically, we want to guarantee that postfix snippets always takes 197 /// precedence over everything else. 198 Exact, 199 } 200 201 impl CompletionRelevance { 202 /// Provides a relevance score. Higher values are more relevant. 203 /// 204 /// The absolute value of the relevance score is not meaningful, for 205 /// example a value of 0 doesn't mean "not relevant", rather 206 /// it means "least relevant". The score value should only be used 207 /// for relative ordering. 208 /// 209 /// See is_relevant if you need to make some judgement about score 210 /// in an absolute sense. score(self) -> u32211 pub fn score(self) -> u32 { 212 let mut score = 0; 213 let CompletionRelevance { 214 exact_name_match, 215 type_match, 216 is_local, 217 is_item_from_trait, 218 is_name_already_imported, 219 requires_import, 220 is_op_method, 221 is_private_editable, 222 postfix_match, 223 is_definite, 224 } = self; 225 226 // lower rank private things 227 if !is_private_editable { 228 score += 1; 229 } 230 // lower rank trait op methods 231 if !is_op_method { 232 score += 10; 233 } 234 // lower rank for conflicting import names 235 if !is_name_already_imported { 236 score += 1; 237 } 238 // lower rank for items that don't need an import 239 if !requires_import { 240 score += 1; 241 } 242 if exact_name_match { 243 score += 10; 244 } 245 score += match postfix_match { 246 Some(CompletionRelevancePostfixMatch::Exact) => 100, 247 Some(CompletionRelevancePostfixMatch::NonExact) => 0, 248 None => 3, 249 }; 250 score += match type_match { 251 Some(CompletionRelevanceTypeMatch::Exact) => 8, 252 Some(CompletionRelevanceTypeMatch::CouldUnify) => 3, 253 None => 0, 254 }; 255 // slightly prefer locals 256 if is_local { 257 score += 1; 258 } 259 if is_item_from_trait { 260 score += 1; 261 } 262 if is_definite { 263 score += 10; 264 } 265 score 266 } 267 268 /// Returns true when the score for this threshold is above 269 /// some threshold such that we think it is especially likely 270 /// to be relevant. is_relevant(&self) -> bool271 pub fn is_relevant(&self) -> bool { 272 self.score() > 0 273 } 274 } 275 276 /// The type of the completion item. 277 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 278 pub enum CompletionItemKind { 279 SymbolKind(SymbolKind), 280 Binding, 281 BuiltinType, 282 InferredType, 283 Keyword, 284 Method, 285 Snippet, 286 UnresolvedReference, 287 } 288 289 impl_from!(SymbolKind for CompletionItemKind); 290 291 impl CompletionItemKind { 292 #[cfg(test)] tag(self) -> &'static str293 pub(crate) fn tag(self) -> &'static str { 294 match self { 295 CompletionItemKind::SymbolKind(kind) => match kind { 296 SymbolKind::Attribute => "at", 297 SymbolKind::BuiltinAttr => "ba", 298 SymbolKind::Const => "ct", 299 SymbolKind::ConstParam => "cp", 300 SymbolKind::Derive => "de", 301 SymbolKind::DeriveHelper => "dh", 302 SymbolKind::Enum => "en", 303 SymbolKind::Field => "fd", 304 SymbolKind::Function => "fn", 305 SymbolKind::Impl => "im", 306 SymbolKind::Label => "lb", 307 SymbolKind::LifetimeParam => "lt", 308 SymbolKind::Local => "lc", 309 SymbolKind::Macro => "ma", 310 SymbolKind::Module => "md", 311 SymbolKind::SelfParam => "sp", 312 SymbolKind::SelfType => "sy", 313 SymbolKind::Static => "sc", 314 SymbolKind::Struct => "st", 315 SymbolKind::ToolModule => "tm", 316 SymbolKind::Trait => "tt", 317 SymbolKind::TraitAlias => "tr", 318 SymbolKind::TypeAlias => "ta", 319 SymbolKind::TypeParam => "tp", 320 SymbolKind::Union => "un", 321 SymbolKind::ValueParam => "vp", 322 SymbolKind::Variant => "ev", 323 }, 324 CompletionItemKind::Binding => "bn", 325 CompletionItemKind::BuiltinType => "bt", 326 CompletionItemKind::InferredType => "it", 327 CompletionItemKind::Keyword => "kw", 328 CompletionItemKind::Method => "me", 329 CompletionItemKind::Snippet => "sn", 330 CompletionItemKind::UnresolvedReference => "??", 331 } 332 } 333 } 334 335 impl CompletionItem { new( kind: impl Into<CompletionItemKind>, source_range: TextRange, label: impl Into<SmolStr>, ) -> Builder336 pub(crate) fn new( 337 kind: impl Into<CompletionItemKind>, 338 source_range: TextRange, 339 label: impl Into<SmolStr>, 340 ) -> Builder { 341 let label = label.into(); 342 Builder { 343 source_range, 344 label, 345 insert_text: None, 346 is_snippet: false, 347 trait_name: None, 348 detail: None, 349 documentation: None, 350 lookup: None, 351 kind: kind.into(), 352 text_edit: None, 353 deprecated: false, 354 trigger_call_info: false, 355 relevance: CompletionRelevance::default(), 356 ref_match: None, 357 imports_to_add: Default::default(), 358 doc_aliases: vec![], 359 } 360 } 361 362 /// What string is used for filtering. lookup(&self) -> &str363 pub fn lookup(&self) -> &str { 364 self.lookup.as_str() 365 } 366 ref_match(&self) -> Option<(String, text_edit::Indel, CompletionRelevance)>367 pub fn ref_match(&self) -> Option<(String, text_edit::Indel, CompletionRelevance)> { 368 // Relevance of the ref match should be the same as the original 369 // match, but with exact type match set because self.ref_match 370 // is only set if there is an exact type match. 371 let mut relevance = self.relevance; 372 relevance.type_match = Some(CompletionRelevanceTypeMatch::Exact); 373 374 self.ref_match.map(|(mutability, offset)| { 375 ( 376 format!("&{}{}", mutability.as_keyword_for_ref(), self.label), 377 text_edit::Indel::insert(offset, format!("&{}", mutability.as_keyword_for_ref())), 378 relevance, 379 ) 380 }) 381 } 382 } 383 384 /// A helper to make `CompletionItem`s. 385 #[must_use] 386 #[derive(Clone)] 387 pub(crate) struct Builder { 388 source_range: TextRange, 389 imports_to_add: SmallVec<[LocatedImport; 1]>, 390 trait_name: Option<SmolStr>, 391 doc_aliases: Vec<SmolStr>, 392 label: SmolStr, 393 insert_text: Option<String>, 394 is_snippet: bool, 395 detail: Option<String>, 396 documentation: Option<Documentation>, 397 lookup: Option<SmolStr>, 398 kind: CompletionItemKind, 399 text_edit: Option<TextEdit>, 400 deprecated: bool, 401 trigger_call_info: bool, 402 relevance: CompletionRelevance, 403 ref_match: Option<(Mutability, TextSize)>, 404 } 405 406 impl Builder { from_resolution( ctx: &CompletionContext<'_>, path_ctx: &PathCompletionCtx, local_name: hir::Name, resolution: hir::ScopeDef, ) -> Self407 pub(crate) fn from_resolution( 408 ctx: &CompletionContext<'_>, 409 path_ctx: &PathCompletionCtx, 410 local_name: hir::Name, 411 resolution: hir::ScopeDef, 412 ) -> Self { 413 let doc_aliases = ctx.doc_aliases_in_scope(resolution); 414 render_path_resolution( 415 RenderContext::new(ctx).doc_aliases(doc_aliases), 416 path_ctx, 417 local_name, 418 resolution, 419 ) 420 } 421 build(self, db: &RootDatabase) -> CompletionItem422 pub(crate) fn build(self, db: &RootDatabase) -> CompletionItem { 423 let _p = profile::span("item::Builder::build"); 424 425 let mut label = self.label; 426 let mut lookup = self.lookup.unwrap_or_else(|| label.clone()); 427 let insert_text = self.insert_text.unwrap_or_else(|| label.to_string()); 428 429 if !self.doc_aliases.is_empty() { 430 let doc_aliases = self.doc_aliases.into_iter().join(", "); 431 label = SmolStr::from(format!("{label} (alias {doc_aliases})")); 432 lookup = SmolStr::from(format!("{lookup} {doc_aliases}")); 433 } 434 if let [import_edit] = &*self.imports_to_add { 435 // snippets can have multiple imports, but normal completions only have up to one 436 if let Some(original_path) = import_edit.original_path.as_ref() { 437 label = SmolStr::from(format!("{label} (use {})", original_path.display(db))); 438 } 439 } else if let Some(trait_name) = self.trait_name { 440 label = SmolStr::from(format!("{label} (as {trait_name})")); 441 } 442 443 let text_edit = match self.text_edit { 444 Some(it) => it, 445 None => TextEdit::replace(self.source_range, insert_text), 446 }; 447 448 let import_to_add = self 449 .imports_to_add 450 .into_iter() 451 .filter_map(|import| { 452 Some(( 453 import.import_path.display(db).to_string(), 454 import.import_path.segments().last()?.display(db).to_string(), 455 )) 456 }) 457 .collect(); 458 459 CompletionItem { 460 source_range: self.source_range, 461 label, 462 text_edit, 463 is_snippet: self.is_snippet, 464 detail: self.detail, 465 documentation: self.documentation, 466 lookup, 467 kind: self.kind, 468 deprecated: self.deprecated, 469 trigger_call_info: self.trigger_call_info, 470 relevance: self.relevance, 471 ref_match: self.ref_match, 472 import_to_add, 473 } 474 } lookup_by(&mut self, lookup: impl Into<SmolStr>) -> &mut Builder475 pub(crate) fn lookup_by(&mut self, lookup: impl Into<SmolStr>) -> &mut Builder { 476 self.lookup = Some(lookup.into()); 477 self 478 } label(&mut self, label: impl Into<SmolStr>) -> &mut Builder479 pub(crate) fn label(&mut self, label: impl Into<SmolStr>) -> &mut Builder { 480 self.label = label.into(); 481 self 482 } trait_name(&mut self, trait_name: SmolStr) -> &mut Builder483 pub(crate) fn trait_name(&mut self, trait_name: SmolStr) -> &mut Builder { 484 self.trait_name = Some(trait_name); 485 self 486 } doc_aliases(&mut self, doc_aliases: Vec<SmolStr>) -> &mut Builder487 pub(crate) fn doc_aliases(&mut self, doc_aliases: Vec<SmolStr>) -> &mut Builder { 488 self.doc_aliases = doc_aliases; 489 self 490 } insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder491 pub(crate) fn insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder { 492 self.insert_text = Some(insert_text.into()); 493 self 494 } insert_snippet( &mut self, cap: SnippetCap, snippet: impl Into<String>, ) -> &mut Builder495 pub(crate) fn insert_snippet( 496 &mut self, 497 cap: SnippetCap, 498 snippet: impl Into<String>, 499 ) -> &mut Builder { 500 let _ = cap; 501 self.is_snippet = true; 502 self.insert_text(snippet) 503 } text_edit(&mut self, edit: TextEdit) -> &mut Builder504 pub(crate) fn text_edit(&mut self, edit: TextEdit) -> &mut Builder { 505 self.text_edit = Some(edit); 506 self 507 } snippet_edit(&mut self, _cap: SnippetCap, edit: TextEdit) -> &mut Builder508 pub(crate) fn snippet_edit(&mut self, _cap: SnippetCap, edit: TextEdit) -> &mut Builder { 509 self.is_snippet = true; 510 self.text_edit(edit) 511 } detail(&mut self, detail: impl Into<String>) -> &mut Builder512 pub(crate) fn detail(&mut self, detail: impl Into<String>) -> &mut Builder { 513 self.set_detail(Some(detail)) 514 } set_detail(&mut self, detail: Option<impl Into<String>>) -> &mut Builder515 pub(crate) fn set_detail(&mut self, detail: Option<impl Into<String>>) -> &mut Builder { 516 self.detail = detail.map(Into::into); 517 if let Some(detail) = &self.detail { 518 if never!(detail.contains('\n'), "multiline detail:\n{}", detail) { 519 self.detail = Some(detail.splitn(2, '\n').next().unwrap().to_string()); 520 } 521 } 522 self 523 } 524 #[allow(unused)] documentation(&mut self, docs: Documentation) -> &mut Builder525 pub(crate) fn documentation(&mut self, docs: Documentation) -> &mut Builder { 526 self.set_documentation(Some(docs)) 527 } set_documentation(&mut self, docs: Option<Documentation>) -> &mut Builder528 pub(crate) fn set_documentation(&mut self, docs: Option<Documentation>) -> &mut Builder { 529 self.documentation = docs.map(Into::into); 530 self 531 } set_deprecated(&mut self, deprecated: bool) -> &mut Builder532 pub(crate) fn set_deprecated(&mut self, deprecated: bool) -> &mut Builder { 533 self.deprecated = deprecated; 534 self 535 } set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder536 pub(crate) fn set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder { 537 self.relevance = relevance; 538 self 539 } trigger_call_info(&mut self) -> &mut Builder540 pub(crate) fn trigger_call_info(&mut self) -> &mut Builder { 541 self.trigger_call_info = true; 542 self 543 } add_import(&mut self, import_to_add: LocatedImport) -> &mut Builder544 pub(crate) fn add_import(&mut self, import_to_add: LocatedImport) -> &mut Builder { 545 self.imports_to_add.push(import_to_add); 546 self 547 } ref_match(&mut self, mutability: Mutability, offset: TextSize) -> &mut Builder548 pub(crate) fn ref_match(&mut self, mutability: Mutability, offset: TextSize) -> &mut Builder { 549 self.ref_match = Some((mutability, offset)); 550 self 551 } 552 } 553 554 #[cfg(test)] 555 mod tests { 556 use itertools::Itertools; 557 use test_utils::assert_eq_text; 558 559 use super::{ 560 CompletionRelevance, CompletionRelevancePostfixMatch, CompletionRelevanceTypeMatch, 561 }; 562 563 /// Check that these are CompletionRelevance are sorted in ascending order 564 /// by their relevance score. 565 /// 566 /// We want to avoid making assertions about the absolute score of any 567 /// item, but we do want to assert whether each is >, <, or == to the 568 /// others. 569 /// 570 /// If provided vec![vec![a], vec![b, c], vec![d]], then this will assert: 571 /// a.score < b.score == c.score < d.score check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>)572 fn check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>) { 573 let expected = format!("{:#?}", &expected_relevance_order); 574 575 let actual_relevance_order = expected_relevance_order 576 .into_iter() 577 .flatten() 578 .map(|r| (r.score(), r)) 579 .sorted_by_key(|(score, _r)| *score) 580 .fold( 581 (u32::MIN, vec![vec![]]), 582 |(mut currently_collecting_score, mut out), (score, r)| { 583 if currently_collecting_score == score { 584 out.last_mut().unwrap().push(r); 585 } else { 586 currently_collecting_score = score; 587 out.push(vec![r]); 588 } 589 (currently_collecting_score, out) 590 }, 591 ) 592 .1; 593 594 let actual = format!("{:#?}", &actual_relevance_order); 595 596 assert_eq_text!(&expected, &actual); 597 } 598 599 #[test] relevance_score()600 fn relevance_score() { 601 use CompletionRelevance as Cr; 602 let default = Cr::default(); 603 // This test asserts that the relevance score for these items is ascending, and 604 // that any items in the same vec have the same score. 605 let expected_relevance_order = vec![ 606 vec![], 607 vec![Cr { is_op_method: true, is_private_editable: true, ..default }], 608 vec![Cr { is_op_method: true, ..default }], 609 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::NonExact), ..default }], 610 vec![Cr { is_private_editable: true, ..default }], 611 vec![default], 612 vec![Cr { is_local: true, ..default }], 613 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::CouldUnify), ..default }], 614 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::Exact), ..default }], 615 vec![Cr { exact_name_match: true, ..default }], 616 vec![Cr { exact_name_match: true, is_local: true, ..default }], 617 vec![Cr { 618 exact_name_match: true, 619 type_match: Some(CompletionRelevanceTypeMatch::Exact), 620 ..default 621 }], 622 vec![Cr { 623 exact_name_match: true, 624 type_match: Some(CompletionRelevanceTypeMatch::Exact), 625 is_local: true, 626 ..default 627 }], 628 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::Exact), ..default }], 629 ]; 630 631 check_relevance_score_ordered(expected_relevance_order); 632 } 633 } 634