1use std::{fmt, mem};
4
5use hir::Mutability;
6use ide_db::text_edit::TextEdit;
7use ide_db::{
8 RootDatabase, SnippetCap, SymbolKind, documentation::Documentation,
9 imports::import_assets::LocatedImport,
10};
11use itertools::Itertools;
12use macros::UpmapFromRaFixture;
13use smallvec::SmallVec;
14use stdx::{format_to, impl_from, never};
15use syntax::{Edition, SmolStr, TextRange, TextSize, format_smolstr};
16
17use crate::{
18 context::{CompletionContext, PathCompletionCtx},
19 render::{RenderContext, render_path_resolution},
20};
21
22#[derive(Clone, UpmapFromRaFixture)]
28#[non_exhaustive]
29pub struct CompletionItem {
30 pub label: CompletionItemLabel,
32
33 pub source_range: TextRange,
42 pub text_edit: TextEdit,
46 pub is_snippet: bool,
47
48 pub kind: CompletionItemKind,
50
51 pub lookup: SmolStr,
57
58 pub detail: Option<String>,
60 pub documentation: Option<Documentation<'static>>,
62
63 pub deprecated: bool,
65
66 pub trigger_call_info: bool,
69
70 pub relevance: CompletionRelevance,
78
79 pub ref_match: Option<(CompletionItemRefMode, TextSize)>,
85
86 pub import_to_add: SmallVec<[String; 1]>,
88}
89
90#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
91pub struct CompletionItemLabel {
92 pub primary: SmolStr,
94 pub detail_left: Option<String>,
96 pub detail_right: Option<String>,
98}
99impl fmt::Debug for CompletionItem {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 let mut s = f.debug_struct("CompletionItem");
103 s.field("label", &self.label.primary)
104 .field("detail_left", &self.label.detail_left)
105 .field("detail_right", &self.label.detail_right)
106 .field("source_range", &self.source_range);
107 if self.text_edit.len() == 1 {
108 let atom = self.text_edit.iter().next().unwrap();
109 s.field("delete", &atom.delete);
110 s.field("insert", &atom.insert);
111 } else {
112 s.field("text_edit", &self.text_edit);
113 }
114 s.field("kind", &self.kind);
115 if self.lookup() != self.label.primary {
116 s.field("lookup", &self.lookup());
117 }
118 if let Some(detail) = &self.detail {
119 s.field("detail", &detail);
120 }
121 if let Some(documentation) = &self.documentation {
122 s.field("documentation", &documentation);
123 }
124 if self.deprecated {
125 s.field("deprecated", &true);
126 }
127
128 if self.relevance != CompletionRelevance::default() {
129 s.field("relevance", &self.relevance);
130 }
131
132 if let Some((ref_mode, offset)) = self.ref_match {
133 let prefix = match ref_mode {
134 CompletionItemRefMode::Reference(mutability) => match mutability {
135 Mutability::Shared => "&",
136 Mutability::Mut => "&mut ",
137 },
138 CompletionItemRefMode::Dereference => "*",
139 };
140 s.field("ref_match", &format!("{prefix}@{offset:?}"));
141 }
142 if self.trigger_call_info {
143 s.field("trigger_call_info", &true);
144 }
145 s.finish()
146 }
147}
148
149#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
150pub struct CompletionRelevance {
151 pub exact_name_match: bool,
162 pub type_match: Option<CompletionRelevanceTypeMatch>,
164 pub is_local: bool,
173 pub trait_: Option<CompletionRelevanceTraitInfo>,
175 pub is_name_already_imported: bool,
177 pub requires_import: bool,
179 pub is_private_editable: bool,
181 pub postfix_match: Option<CompletionRelevancePostfixMatch>,
183 pub function: Option<CompletionRelevanceFn>,
185 pub is_skipping_completion: bool,
187}
188#[derive(Debug, Clone, Copy, Eq, PartialEq)]
189pub struct CompletionRelevanceTraitInfo {
190 pub notable_trait: bool,
192 pub is_op_method: bool,
194}
195
196#[derive(Debug, Clone, Copy, Eq, PartialEq)]
197pub enum CompletionRelevanceTypeMatch {
198 CouldUnify,
208 Exact,
218}
219
220#[derive(Debug, Clone, Copy, Eq, PartialEq)]
221pub enum CompletionRelevancePostfixMatch {
222 NonExact,
224 Exact,
233}
234
235#[derive(Debug, Clone, Copy, Eq, PartialEq)]
236pub struct CompletionRelevanceFn {
237 pub has_params: bool,
238 pub has_self_param: bool,
239 pub return_type: CompletionRelevanceReturnType,
240}
241
242#[derive(Debug, Clone, Copy, Eq, PartialEq)]
243pub enum CompletionRelevanceReturnType {
244 Other,
245 DirectConstructor,
247 Constructor,
249 Builder,
251}
252
253impl CompletionRelevance {
254 const BASE_SCORE: u32 = u32::MAX / 2;
264
265 pub fn score(self) -> u32 {
266 let mut score = Self::BASE_SCORE;
267 let CompletionRelevance {
268 exact_name_match,
269 type_match,
270 is_local,
271 is_name_already_imported,
272 requires_import,
273 is_private_editable,
274 postfix_match,
275 trait_,
276 function,
277 is_skipping_completion,
278 } = self;
279
280 if is_name_already_imported {
283 score -= 1;
284 }
285 if is_local {
287 score += 1;
288 }
289
290 if !is_private_editable {
292 score += 1;
293 }
294
295 if let Some(trait_) = trait_ {
296 if !trait_.notable_trait {
298 score -= 5;
299 }
300 if trait_.is_op_method {
302 score -= 5;
303 }
304 }
305
306 if is_skipping_completion {
308 score -= 7;
309 }
310
311 if requires_import {
313 score -= 1;
314 }
315 if exact_name_match {
316 score += 20;
317 }
318 match postfix_match {
319 Some(CompletionRelevancePostfixMatch::Exact) => score += 100,
320 Some(CompletionRelevancePostfixMatch::NonExact) => score -= 5,
321 None => (),
322 };
323 score += match type_match {
324 Some(CompletionRelevanceTypeMatch::Exact) => 18,
325 Some(CompletionRelevanceTypeMatch::CouldUnify) => 5,
326 None => 0,
327 };
328 if let Some(function) = function {
329 let mut fn_score = match function.return_type {
330 CompletionRelevanceReturnType::DirectConstructor => 15,
331 CompletionRelevanceReturnType::Builder => 10,
332 CompletionRelevanceReturnType::Constructor => 5,
333 CompletionRelevanceReturnType::Other => 0u32,
334 };
335
336 if function.has_params {
340 fn_score = fn_score.saturating_sub(1);
342 } else if function.has_self_param {
343 fn_score = fn_score.min(1);
345 }
346
347 score += fn_score;
348 };
349
350 score
351 }
352
353 pub fn is_relevant(&self) -> bool {
357 self.score() > Self::BASE_SCORE
358 }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
363pub enum CompletionItemKind {
364 SymbolKind(SymbolKind),
365 Binding,
366 BuiltinType,
367 InferredType,
368 Keyword,
369 Snippet,
370 UnresolvedReference,
371 Expression,
372}
373
374impl_from!(SymbolKind for CompletionItemKind);
375
376impl CompletionItemKind {
377 pub fn tag(self) -> &'static str {
378 match self {
379 CompletionItemKind::SymbolKind(kind) => match kind {
380 SymbolKind::Attribute => "at",
381 SymbolKind::BuiltinAttr => "ba",
382 SymbolKind::Const => "ct",
383 SymbolKind::ConstParam => "cp",
384 SymbolKind::Derive => "de",
385 SymbolKind::DeriveHelper => "dh",
386 SymbolKind::Enum => "en",
387 SymbolKind::Field => "fd",
388 SymbolKind::Function => "fn",
389 SymbolKind::Impl => "im",
390 SymbolKind::InlineAsmRegOrRegClass => "ar",
391 SymbolKind::Label => "lb",
392 SymbolKind::LifetimeParam => "lt",
393 SymbolKind::Local => "lc",
394 SymbolKind::Macro => "ma",
395 SymbolKind::Method => "me",
396 SymbolKind::ProcMacro => "pm",
397 SymbolKind::Module => "md",
398 SymbolKind::SelfParam => "sp",
399 SymbolKind::SelfType => "sy",
400 SymbolKind::Static => "sc",
401 SymbolKind::Struct => "st",
402 SymbolKind::ToolModule => "tm",
403 SymbolKind::Trait => "tt",
404 SymbolKind::TypeAlias => "ta",
405 SymbolKind::TypeParam => "tp",
406 SymbolKind::Union => "un",
407 SymbolKind::ValueParam => "vp",
408 SymbolKind::Variant => "ev",
409 },
410 CompletionItemKind::Binding => "bn",
411 CompletionItemKind::BuiltinType => "bt",
412 CompletionItemKind::InferredType => "it",
413 CompletionItemKind::Keyword => "kw",
414 CompletionItemKind::Snippet => "sn",
415 CompletionItemKind::UnresolvedReference => "??",
416 CompletionItemKind::Expression => "ex",
417 }
418 }
419}
420
421#[derive(Copy, Clone, Debug)]
422pub enum CompletionItemRefMode {
423 Reference(Mutability),
424 Dereference,
425}
426
427impl CompletionItem {
428 pub(crate) fn new(
429 kind: impl Into<CompletionItemKind>,
430 source_range: TextRange,
431 label: impl Into<SmolStr>,
432 edition: Edition,
433 ) -> Builder {
434 let label = label.into();
435 Builder {
436 source_range,
437 label,
438 insert_text: None,
439 is_snippet: false,
440 trait_name: None,
441 detail: None,
442 documentation: None,
443 lookup: None,
444 kind: kind.into(),
445 text_edit: None,
446 deprecated: false,
447 trigger_call_info: false,
448 relevance: CompletionRelevance::default(),
449 ref_match: None,
450 imports_to_add: Default::default(),
451 doc_aliases: vec![],
452 edition,
453 }
454 }
455
456 pub fn lookup(&self) -> &str {
458 self.lookup.as_str()
459 }
460
461 pub fn ref_match(&self) -> Option<(String, ide_db::text_edit::Indel, CompletionRelevance)> {
462 let mut relevance = self.relevance;
466 relevance.type_match = Some(CompletionRelevanceTypeMatch::Exact);
467
468 self.ref_match.map(|(mode, offset)| {
469 let prefix = match mode {
470 CompletionItemRefMode::Reference(Mutability::Shared) => "&",
471 CompletionItemRefMode::Reference(Mutability::Mut) => "&mut ",
472 CompletionItemRefMode::Dereference => "*",
473 };
474 let label = format!("{prefix}{}", self.label.primary);
475 (label, ide_db::text_edit::Indel::insert(offset, String::from(prefix)), relevance)
476 })
477 }
478}
479
480#[must_use]
482#[derive(Clone)]
483pub(crate) struct Builder {
484 source_range: TextRange,
485 imports_to_add: SmallVec<[LocatedImport; 1]>,
486 trait_name: Option<SmolStr>,
487 doc_aliases: Vec<SmolStr>,
488 label: SmolStr,
489 insert_text: Option<String>,
490 is_snippet: bool,
491 detail: Option<String>,
492 documentation: Option<Documentation<'static>>,
494 lookup: Option<SmolStr>,
495 kind: CompletionItemKind,
496 text_edit: Option<TextEdit>,
497 deprecated: bool,
498 trigger_call_info: bool,
499 relevance: CompletionRelevance,
500 ref_match: Option<(CompletionItemRefMode, TextSize)>,
501 edition: Edition,
502}
503
504impl Builder {
505 pub(crate) fn from_resolution(
506 ctx: &CompletionContext<'_>,
507 path_ctx: &PathCompletionCtx<'_>,
508 local_name: hir::Name,
509 resolution: hir::ScopeDef,
510 ) -> Self {
511 let doc_aliases = ctx.doc_aliases_in_scope(resolution);
512 render_path_resolution(
513 RenderContext::new(ctx).doc_aliases(doc_aliases),
514 path_ctx,
515 local_name,
516 resolution,
517 )
518 }
519
520 pub(crate) fn build(self, db: &RootDatabase) -> CompletionItem {
521 let _p = tracing::info_span!("item::Builder::build").entered();
522
523 let label = self.label;
524 let mut lookup = self.lookup.unwrap_or_else(|| label.clone());
525 let insert_text = self.insert_text.unwrap_or_else(|| label.to_string());
526
527 let mut detail_left = None;
528 if !self.doc_aliases.is_empty() {
529 let doc_aliases = self.doc_aliases.iter().join(", ");
530 detail_left = Some(format!("(alias {doc_aliases})"));
531 let lookup_doc_aliases = self
532 .doc_aliases
533 .iter()
534 .filter(|alias| {
538 let mut chars = alias.chars();
539 chars.next().is_some_and(char::is_alphabetic)
540 && chars.all(|c| c.is_alphanumeric() || c == '_')
541 })
542 .join("");
546 if !lookup_doc_aliases.is_empty() {
547 lookup = format_smolstr!("{lookup}{lookup_doc_aliases}");
548 }
549 }
550 if let [import_edit] = &*self.imports_to_add {
551 let detail_left = detail_left.get_or_insert_with(String::new);
553 format_to!(
554 detail_left,
555 "{}(use {})",
556 if detail_left.is_empty() { "" } else { " " },
557 import_edit.import_path.display(db, self.edition)
558 );
559 } else if let Some(trait_name) = self.trait_name {
560 let detail_left = detail_left.get_or_insert_with(String::new);
561 format_to!(
562 detail_left,
563 "{}(as {trait_name})",
564 if detail_left.is_empty() { "" } else { " " },
565 );
566 }
567
568 let text_edit = match self.text_edit {
569 Some(it) => it,
570 None => TextEdit::replace(self.source_range, insert_text),
571 };
572
573 let import_to_add = self
574 .imports_to_add
575 .into_iter()
576 .map(|import| import.import_path.display(db, self.edition).to_string())
577 .collect();
578
579 CompletionItem {
580 source_range: self.source_range,
581 label: CompletionItemLabel {
582 primary: label,
583 detail_left,
584 detail_right: self.detail.clone(),
585 },
586 text_edit,
587 is_snippet: self.is_snippet,
588 detail: self.detail,
589 documentation: self.documentation,
590 lookup,
591 kind: self.kind,
592 deprecated: self.deprecated,
593 trigger_call_info: self.trigger_call_info,
594 relevance: self.relevance,
595 ref_match: self.ref_match,
596 import_to_add,
597 }
598 }
599 pub(crate) fn lookup_by(&mut self, lookup: impl Into<SmolStr>) -> &mut Builder {
600 self.lookup = Some(lookup.into());
601 self
602 }
603 pub(crate) fn label(&mut self, label: impl Into<SmolStr>) -> &mut Builder {
604 self.label = label.into();
605 self
606 }
607 pub(crate) fn trait_name(&mut self, trait_name: SmolStr) -> &mut Builder {
608 self.trait_name = Some(trait_name);
609 self
610 }
611 pub(crate) fn doc_aliases(&mut self, doc_aliases: Vec<SmolStr>) -> &mut Builder {
612 self.doc_aliases = doc_aliases;
613 self
614 }
615 pub(crate) fn insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder {
616 self.insert_text = Some(insert_text.into());
617 self
618 }
619 pub(crate) fn insert_snippet(
620 &mut self,
621 cap: SnippetCap,
622 snippet: impl Into<String>,
623 ) -> &mut Builder {
624 let _ = cap;
625 self.is_snippet = true;
626 self.insert_text(snippet)
627 }
628 pub(crate) fn text_edit(&mut self, edit: TextEdit) -> &mut Builder {
629 self.text_edit = Some(edit);
630 self
631 }
632 pub(crate) fn snippet_edit(&mut self, _cap: SnippetCap, edit: TextEdit) -> &mut Builder {
633 self.is_snippet = true;
634 self.text_edit(edit)
635 }
636 pub(crate) fn detail(&mut self, detail: impl Into<String>) -> &mut Builder {
637 self.set_detail(Some(detail))
638 }
639 pub(crate) fn set_detail(&mut self, detail: Option<impl Into<String>>) -> &mut Builder {
640 self.detail = detail.map(Into::into);
641 if let Some(detail) = &self.detail
642 && never!(detail.contains('\n'), "multiline detail:\n{}", detail)
643 {
644 self.detail = Some(detail.split('\n').next().unwrap().to_owned());
645 }
646 self
647 }
648 #[allow(unused)]
649 pub(crate) fn documentation(&mut self, docs: Documentation<'_>) -> &mut Builder {
650 self.set_documentation(Some(docs))
651 }
652 pub(crate) fn set_documentation(&mut self, docs: Option<Documentation<'_>>) -> &mut Builder {
653 self.documentation = docs.map(Documentation::into_owned);
654 self
655 }
656 pub(crate) fn set_deprecated(&mut self, deprecated: bool) -> &mut Builder {
657 self.deprecated = deprecated;
658 self
659 }
660 pub(crate) fn set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder {
661 self.relevance = relevance;
662 self
663 }
664 pub(crate) fn with_relevance(
665 &mut self,
666 relevance: impl FnOnce(CompletionRelevance) -> CompletionRelevance,
667 ) -> &mut Builder {
668 self.relevance = relevance(mem::take(&mut self.relevance));
669 self
670 }
671 pub(crate) fn trigger_call_info(&mut self) -> &mut Builder {
672 self.trigger_call_info = true;
673 self
674 }
675 pub(crate) fn add_import(&mut self, import_to_add: LocatedImport) -> &mut Builder {
676 self.imports_to_add.push(import_to_add);
677 self
678 }
679 pub(crate) fn ref_match(
680 &mut self,
681 ref_mode: CompletionItemRefMode,
682 offset: TextSize,
683 ) -> &mut Builder {
684 self.ref_match = Some((ref_mode, offset));
685 self
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use itertools::Itertools;
692 use test_utils::assert_eq_text;
693
694 use super::{
695 CompletionRelevance, CompletionRelevancePostfixMatch, CompletionRelevanceTypeMatch,
696 };
697
698 fn check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>) {
708 let expected = format!("{expected_relevance_order:#?}");
709
710 let actual_relevance_order = expected_relevance_order
711 .into_iter()
712 .flatten()
713 .map(|r| (r.score(), r))
714 .sorted_by_key(|(score, _r)| *score)
715 .fold(
716 (u32::MIN, vec![vec![]]),
717 |(mut currently_collecting_score, mut out), (score, r)| {
718 if currently_collecting_score == score {
719 out.last_mut().unwrap().push(r);
720 } else {
721 currently_collecting_score = score;
722 out.push(vec![r]);
723 }
724 (currently_collecting_score, out)
725 },
726 )
727 .1;
728
729 let actual = format!("{actual_relevance_order:#?}");
730
731 assert_eq_text!(&expected, &actual);
732 }
733
734 #[test]
735 fn relevance_score() {
736 use CompletionRelevance as Cr;
737 let default = Cr::default();
738 let expected_relevance_order = vec![
741 vec![],
742 vec![Cr {
743 trait_: Some(crate::item::CompletionRelevanceTraitInfo {
744 notable_trait: false,
745 is_op_method: true,
746 }),
747 is_private_editable: true,
748 ..default
749 }],
750 vec![Cr {
751 trait_: Some(crate::item::CompletionRelevanceTraitInfo {
752 notable_trait: false,
753 is_op_method: true,
754 }),
755 ..default
756 }],
757 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::NonExact), ..default }],
758 vec![Cr { is_private_editable: true, ..default }],
759 vec![default],
760 vec![Cr { is_local: true, ..default }],
761 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::CouldUnify), ..default }],
762 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::Exact), ..default }],
763 vec![Cr { exact_name_match: true, ..default }],
764 vec![Cr { exact_name_match: true, is_local: true, ..default }],
765 vec![Cr {
766 exact_name_match: true,
767 type_match: Some(CompletionRelevanceTypeMatch::Exact),
768 ..default
769 }],
770 vec![Cr {
771 exact_name_match: true,
772 type_match: Some(CompletionRelevanceTypeMatch::Exact),
773 is_local: true,
774 ..default
775 }],
776 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::Exact), ..default }],
777 ];
778
779 check_relevance_score_ordered(expected_relevance_order);
780 }
781}