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 pub has_local_inherent_impl: bool,
189}
190#[derive(Debug, Clone, Copy, Eq, PartialEq)]
191pub struct CompletionRelevanceTraitInfo {
192 pub notable_trait: bool,
194 pub is_op_method: bool,
196}
197
198#[derive(Debug, Clone, Copy, Eq, PartialEq)]
199pub enum CompletionRelevanceTypeMatch {
200 CouldUnify,
210 Exact,
220}
221
222#[derive(Debug, Clone, Copy, Eq, PartialEq)]
223pub enum CompletionRelevancePostfixMatch {
224 NonExact,
226 Exact,
235}
236
237#[derive(Debug, Clone, Copy, Eq, PartialEq)]
238pub struct CompletionRelevanceFn {
239 pub has_params: bool,
240 pub has_self_param: bool,
241 pub return_type: CompletionRelevanceReturnType,
242}
243
244#[derive(Debug, Clone, Copy, Eq, PartialEq)]
245pub enum CompletionRelevanceReturnType {
246 Other,
247 DirectConstructor,
249 Constructor,
251 Builder,
253}
254
255impl CompletionRelevance {
256 const BASE_SCORE: u32 = u32::MAX / 2;
266
267 pub fn score(self) -> u32 {
268 let mut score = Self::BASE_SCORE;
269 let CompletionRelevance {
270 exact_name_match,
271 type_match,
272 is_local,
273 is_name_already_imported,
274 requires_import,
275 is_private_editable,
276 postfix_match,
277 trait_,
278 function,
279 is_skipping_completion,
280 has_local_inherent_impl,
281 } = self;
282
283 if is_name_already_imported {
286 score -= 1;
287 }
288 if is_local {
290 score += 1;
291 }
292
293 if !is_private_editable {
295 score += 1;
296 }
297
298 if let Some(trait_) = trait_ {
299 if !trait_.notable_trait {
301 score -= 5;
302 }
303 if trait_.is_op_method {
305 score -= 5;
306 }
307 }
308
309 if is_skipping_completion {
311 score -= 7;
312 }
313
314 if requires_import {
316 score -= 1;
317 }
318 if exact_name_match {
319 score += 20;
320 }
321 match postfix_match {
322 Some(CompletionRelevancePostfixMatch::Exact) => score += 100,
323 Some(CompletionRelevancePostfixMatch::NonExact) => score -= 5,
324 None => (),
325 };
326 score += match type_match {
327 Some(CompletionRelevanceTypeMatch::Exact) => 18,
328 Some(CompletionRelevanceTypeMatch::CouldUnify) => 5,
329 None => 0,
330 };
331 if let Some(function) = function {
332 let mut fn_score = match function.return_type {
333 CompletionRelevanceReturnType::DirectConstructor => 15,
334 CompletionRelevanceReturnType::Builder => 10,
335 CompletionRelevanceReturnType::Constructor => 5,
336 CompletionRelevanceReturnType::Other => 0u32,
337 };
338
339 if function.has_params {
343 fn_score = fn_score.saturating_sub(1);
345 } else if function.has_self_param {
346 fn_score = fn_score.min(1);
348 }
349
350 score += fn_score;
351 };
352
353 if has_local_inherent_impl {
354 score -= 5;
355 }
356
357 score
358 }
359
360 pub fn is_relevant(&self) -> bool {
364 self.score() > Self::BASE_SCORE
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
370pub enum CompletionItemKind {
371 SymbolKind(SymbolKind),
372 Binding,
373 BuiltinType,
374 InferredType,
375 Keyword,
376 Snippet,
377 UnresolvedReference,
378 Expression,
379}
380
381impl_from!(SymbolKind for CompletionItemKind);
382
383impl CompletionItemKind {
384 pub fn tag(self) -> &'static str {
385 match self {
386 CompletionItemKind::SymbolKind(kind) => match kind {
387 SymbolKind::Attribute => "at",
388 SymbolKind::BuiltinAttr => "ba",
389 SymbolKind::Const => "ct",
390 SymbolKind::ConstParam => "cp",
391 SymbolKind::CrateRoot => "cr",
392 SymbolKind::Derive => "de",
393 SymbolKind::DeriveHelper => "dh",
394 SymbolKind::Enum => "en",
395 SymbolKind::Field => "fd",
396 SymbolKind::Function => "fn",
397 SymbolKind::Impl => "im",
398 SymbolKind::InlineAsmRegOrRegClass => "ar",
399 SymbolKind::Label => "lb",
400 SymbolKind::LifetimeParam => "lt",
401 SymbolKind::Local => "lc",
402 SymbolKind::Macro => "ma",
403 SymbolKind::Method => "me",
404 SymbolKind::ProcMacro => "pm",
405 SymbolKind::Module => "md",
406 SymbolKind::SelfParam => "sp",
407 SymbolKind::SelfType => "sy",
408 SymbolKind::Static => "sc",
409 SymbolKind::Struct => "st",
410 SymbolKind::ToolModule => "tm",
411 SymbolKind::Trait => "tt",
412 SymbolKind::TypeAlias => "ta",
413 SymbolKind::TypeParam => "tp",
414 SymbolKind::Union => "un",
415 SymbolKind::ValueParam => "vp",
416 SymbolKind::Variant => "ev",
417 },
418 CompletionItemKind::Binding => "bn",
419 CompletionItemKind::BuiltinType => "bt",
420 CompletionItemKind::InferredType => "it",
421 CompletionItemKind::Keyword => "kw",
422 CompletionItemKind::Snippet => "sn",
423 CompletionItemKind::UnresolvedReference => "??",
424 CompletionItemKind::Expression => "ex",
425 }
426 }
427}
428
429#[derive(Copy, Clone, Debug)]
430pub enum CompletionItemRefMode {
431 Reference(Mutability),
432 Dereference,
433}
434
435impl CompletionItem {
436 pub(crate) fn new(
437 kind: impl Into<CompletionItemKind>,
438 source_range: TextRange,
439 label: impl Into<SmolStr>,
440 edition: Edition,
441 ) -> Builder {
442 let label = label.into();
443 Builder {
444 source_range,
445 label,
446 insert_text: None,
447 is_snippet: false,
448 trait_name: None,
449 detail: None,
450 documentation: None,
451 lookup: None,
452 kind: kind.into(),
453 text_edit: None,
454 deprecated: false,
455 trigger_call_info: false,
456 relevance: CompletionRelevance::default(),
457 ref_match: None,
458 imports_to_add: Default::default(),
459 doc_aliases: vec![],
460 adds_text: None,
461 edition,
462 }
463 }
464
465 pub fn lookup(&self) -> &str {
467 self.lookup.as_str()
468 }
469
470 pub fn ref_match(&self) -> Option<(String, ide_db::text_edit::Indel, CompletionRelevance)> {
471 let mut relevance = self.relevance;
475 relevance.type_match = Some(CompletionRelevanceTypeMatch::Exact);
476
477 self.ref_match.map(|(mode, offset)| {
478 let prefix = match mode {
479 CompletionItemRefMode::Reference(Mutability::Shared) => "&",
480 CompletionItemRefMode::Reference(Mutability::Mut) => "&mut ",
481 CompletionItemRefMode::Dereference => "*",
482 };
483 let label = format!("{prefix}{}", self.label.primary);
484 (label, ide_db::text_edit::Indel::insert(offset, String::from(prefix)), relevance)
485 })
486 }
487}
488
489#[must_use]
491#[derive(Debug, Clone)]
492pub(crate) struct Builder {
493 source_range: TextRange,
494 imports_to_add: SmallVec<[LocatedImport; 1]>,
495 trait_name: Option<SmolStr>,
496 doc_aliases: Vec<SmolStr>,
497 adds_text: Option<SmolStr>,
498 label: SmolStr,
499 insert_text: Option<String>,
500 is_snippet: bool,
501 detail: Option<String>,
502 documentation: Option<Documentation<'static>>,
504 lookup: Option<SmolStr>,
505 kind: CompletionItemKind,
506 text_edit: Option<TextEdit>,
507 deprecated: bool,
508 trigger_call_info: bool,
509 relevance: CompletionRelevance,
510 ref_match: Option<(CompletionItemRefMode, TextSize)>,
511 edition: Edition,
512}
513
514impl Builder {
515 pub(crate) fn from_resolution(
516 ctx: &CompletionContext<'_>,
517 path_ctx: &PathCompletionCtx<'_>,
518 local_name: hir::Name,
519 resolution: hir::ScopeDef,
520 ) -> Self {
521 let doc_aliases = ctx.doc_aliases_in_scope(resolution);
522 render_path_resolution(
523 RenderContext::new(ctx).doc_aliases(doc_aliases),
524 path_ctx,
525 local_name,
526 resolution,
527 )
528 }
529
530 pub(crate) fn build(self, db: &RootDatabase) -> CompletionItem {
531 let _p = tracing::info_span!("item::Builder::build").entered();
532
533 let label = self.label;
534 let mut lookup = self.lookup.unwrap_or_else(|| label.clone());
535 let insert_text = self.insert_text.unwrap_or_else(|| label.to_string());
536
537 let mut detail_left = None;
538 let mut to_detail_left = |args: fmt::Arguments<'_>| {
539 let detail_left = detail_left.get_or_insert_with(String::new);
540 if !detail_left.is_empty() {
541 detail_left.push(' ');
542 }
543 format_to!(detail_left, "{args}")
544 };
545 if !self.doc_aliases.is_empty() {
546 let doc_aliases = self.doc_aliases.iter().join(", ");
547 to_detail_left(format_args!("(alias {doc_aliases})"));
548 let lookup_doc_aliases = self
549 .doc_aliases
550 .iter()
551 .filter(|alias| {
555 let mut chars = alias.chars();
556 chars.next().is_some_and(char::is_alphabetic)
557 && chars.all(|c| c.is_alphanumeric() || c == '_')
558 })
559 .join("");
563 if !lookup_doc_aliases.is_empty() {
564 lookup = format_smolstr!("{lookup}{lookup_doc_aliases}");
565 }
566 }
567 if let Some(adds_text) = self.adds_text {
568 to_detail_left(format_args!("(adds {})", adds_text.trim()));
569 }
570 if let [import_edit] = &*self.imports_to_add {
571 to_detail_left(format_args!(
573 "(use {})",
574 import_edit.import_path.display(db, self.edition)
575 ));
576 } else if let Some(trait_name) = self.trait_name {
577 to_detail_left(format_args!("(as {trait_name})"));
578 }
579
580 let text_edit = match self.text_edit {
581 Some(it) => it,
582 None => TextEdit::replace(self.source_range, insert_text),
583 };
584
585 let import_to_add = self
586 .imports_to_add
587 .into_iter()
588 .map(|import| import.import_path.display(db, self.edition).to_string())
589 .collect();
590
591 CompletionItem {
592 source_range: self.source_range,
593 label: CompletionItemLabel {
594 primary: label,
595 detail_left,
596 detail_right: self.detail.clone(),
597 },
598 text_edit,
599 is_snippet: self.is_snippet,
600 detail: self.detail,
601 documentation: self.documentation,
602 lookup,
603 kind: self.kind,
604 deprecated: self.deprecated,
605 trigger_call_info: self.trigger_call_info,
606 relevance: self.relevance,
607 ref_match: self.ref_match,
608 import_to_add,
609 }
610 }
611 pub(crate) fn lookup_by(&mut self, lookup: impl Into<SmolStr>) -> &mut Builder {
612 self.lookup = Some(lookup.into());
613 self
614 }
615 pub(crate) fn label(&mut self, label: impl Into<SmolStr>) -> &mut Builder {
616 self.label = label.into();
617 self
618 }
619 pub(crate) fn trait_name(&mut self, trait_name: SmolStr) -> &mut Builder {
620 self.trait_name = Some(trait_name);
621 self
622 }
623 pub(crate) fn doc_aliases(&mut self, doc_aliases: Vec<SmolStr>) -> &mut Builder {
624 self.doc_aliases = doc_aliases;
625 self
626 }
627 pub(crate) fn adds_text(&mut self, adds_text: SmolStr) -> &mut Builder {
628 self.adds_text = Some(adds_text);
629 self
630 }
631 pub(crate) fn insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder {
632 self.insert_text = Some(insert_text.into());
633 self
634 }
635 pub(crate) fn insert_snippet(
636 &mut self,
637 cap: SnippetCap,
638 snippet: impl Into<String>,
639 ) -> &mut Builder {
640 let _ = cap;
641 self.is_snippet = true;
642 self.insert_text(snippet)
643 }
644 pub(crate) fn text_edit(&mut self, edit: TextEdit) -> &mut Builder {
645 self.text_edit = Some(edit);
646 self
647 }
648 pub(crate) fn snippet_edit(&mut self, _cap: SnippetCap, edit: TextEdit) -> &mut Builder {
649 self.is_snippet = true;
650 self.text_edit(edit)
651 }
652 pub(crate) fn detail(&mut self, detail: impl Into<String>) -> &mut Builder {
653 self.set_detail(Some(detail))
654 }
655 pub(crate) fn set_detail(&mut self, detail: Option<impl Into<String>>) -> &mut Builder {
656 self.detail = detail.map(Into::into);
657 if let Some(detail) = &self.detail
658 && never!(detail.contains('\n'), "multiline detail:\n{}", detail)
659 {
660 self.detail = Some(detail.split('\n').next().unwrap().to_owned());
661 }
662 self
663 }
664 #[allow(unused)]
665 pub(crate) fn documentation(&mut self, docs: Documentation<'_>) -> &mut Builder {
666 self.set_documentation(Some(docs))
667 }
668 pub(crate) fn set_documentation(&mut self, docs: Option<Documentation<'_>>) -> &mut Builder {
669 self.documentation = docs.map(Documentation::into_owned);
670 self
671 }
672 pub(crate) fn set_deprecated(&mut self, deprecated: bool) -> &mut Builder {
673 self.deprecated = deprecated;
674 self
675 }
676 pub(crate) fn set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder {
677 self.relevance = relevance;
678 self
679 }
680 pub(crate) fn with_relevance(
681 &mut self,
682 relevance: impl FnOnce(CompletionRelevance) -> CompletionRelevance,
683 ) -> &mut Builder {
684 self.relevance = relevance(mem::take(&mut self.relevance));
685 self
686 }
687 pub(crate) fn trigger_call_info(&mut self) -> &mut Builder {
688 self.trigger_call_info = true;
689 self
690 }
691 pub(crate) fn add_import(&mut self, import_to_add: LocatedImport) -> &mut Builder {
692 self.imports_to_add.push(import_to_add);
693 self
694 }
695 pub(crate) fn ref_match(
696 &mut self,
697 ref_mode: CompletionItemRefMode,
698 offset: TextSize,
699 ) -> &mut Builder {
700 self.ref_match = Some((ref_mode, offset));
701 self
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use itertools::Itertools;
708 use test_utils::assert_eq_text;
709
710 use super::{
711 CompletionRelevance, CompletionRelevancePostfixMatch, CompletionRelevanceTypeMatch,
712 };
713
714 fn check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>) {
724 let expected = format!("{expected_relevance_order:#?}");
725
726 let actual_relevance_order = expected_relevance_order
727 .into_iter()
728 .flatten()
729 .map(|r| (r.score(), r))
730 .sorted_by_key(|(score, _r)| *score)
731 .fold(
732 (u32::MIN, vec![vec![]]),
733 |(mut currently_collecting_score, mut out), (score, r)| {
734 if currently_collecting_score == score {
735 out.last_mut().unwrap().push(r);
736 } else {
737 currently_collecting_score = score;
738 out.push(vec![r]);
739 }
740 (currently_collecting_score, out)
741 },
742 )
743 .1;
744
745 let actual = format!("{actual_relevance_order:#?}");
746
747 assert_eq_text!(&expected, &actual);
748 }
749
750 #[test]
751 fn relevance_score() {
752 use CompletionRelevance as Cr;
753 let default = Cr::default();
754 let expected_relevance_order = vec![
757 vec![],
758 vec![Cr {
759 trait_: Some(crate::item::CompletionRelevanceTraitInfo {
760 notable_trait: false,
761 is_op_method: true,
762 }),
763 is_private_editable: true,
764 ..default
765 }],
766 vec![Cr {
767 trait_: Some(crate::item::CompletionRelevanceTraitInfo {
768 notable_trait: false,
769 is_op_method: true,
770 }),
771 ..default
772 }],
773 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::NonExact), ..default }],
774 vec![Cr { is_private_editable: true, ..default }],
775 vec![default],
776 vec![Cr { is_local: true, ..default }],
777 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::CouldUnify), ..default }],
778 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::Exact), ..default }],
779 vec![Cr { exact_name_match: true, ..default }],
780 vec![Cr { exact_name_match: true, is_local: true, ..default }],
781 vec![Cr {
782 exact_name_match: true,
783 type_match: Some(CompletionRelevanceTypeMatch::Exact),
784 ..default
785 }],
786 vec![Cr {
787 exact_name_match: true,
788 type_match: Some(CompletionRelevanceTypeMatch::Exact),
789 is_local: true,
790 ..default
791 }],
792 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::Exact), ..default }],
793 ];
794
795 check_relevance_score_ordered(expected_relevance_order);
796 }
797}