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::CrateRoot => "cr",
385 SymbolKind::Derive => "de",
386 SymbolKind::DeriveHelper => "dh",
387 SymbolKind::Enum => "en",
388 SymbolKind::Field => "fd",
389 SymbolKind::Function => "fn",
390 SymbolKind::Impl => "im",
391 SymbolKind::InlineAsmRegOrRegClass => "ar",
392 SymbolKind::Label => "lb",
393 SymbolKind::LifetimeParam => "lt",
394 SymbolKind::Local => "lc",
395 SymbolKind::Macro => "ma",
396 SymbolKind::Method => "me",
397 SymbolKind::ProcMacro => "pm",
398 SymbolKind::Module => "md",
399 SymbolKind::SelfParam => "sp",
400 SymbolKind::SelfType => "sy",
401 SymbolKind::Static => "sc",
402 SymbolKind::Struct => "st",
403 SymbolKind::ToolModule => "tm",
404 SymbolKind::Trait => "tt",
405 SymbolKind::TypeAlias => "ta",
406 SymbolKind::TypeParam => "tp",
407 SymbolKind::Union => "un",
408 SymbolKind::ValueParam => "vp",
409 SymbolKind::Variant => "ev",
410 },
411 CompletionItemKind::Binding => "bn",
412 CompletionItemKind::BuiltinType => "bt",
413 CompletionItemKind::InferredType => "it",
414 CompletionItemKind::Keyword => "kw",
415 CompletionItemKind::Snippet => "sn",
416 CompletionItemKind::UnresolvedReference => "??",
417 CompletionItemKind::Expression => "ex",
418 }
419 }
420}
421
422#[derive(Copy, Clone, Debug)]
423pub enum CompletionItemRefMode {
424 Reference(Mutability),
425 Dereference,
426}
427
428impl CompletionItem {
429 pub(crate) fn new(
430 kind: impl Into<CompletionItemKind>,
431 source_range: TextRange,
432 label: impl Into<SmolStr>,
433 edition: Edition,
434 ) -> Builder {
435 let label = label.into();
436 Builder {
437 source_range,
438 label,
439 insert_text: None,
440 is_snippet: false,
441 trait_name: None,
442 detail: None,
443 documentation: None,
444 lookup: None,
445 kind: kind.into(),
446 text_edit: None,
447 deprecated: false,
448 trigger_call_info: false,
449 relevance: CompletionRelevance::default(),
450 ref_match: None,
451 imports_to_add: Default::default(),
452 doc_aliases: vec![],
453 edition,
454 }
455 }
456
457 pub fn lookup(&self) -> &str {
459 self.lookup.as_str()
460 }
461
462 pub fn ref_match(&self) -> Option<(String, ide_db::text_edit::Indel, CompletionRelevance)> {
463 let mut relevance = self.relevance;
467 relevance.type_match = Some(CompletionRelevanceTypeMatch::Exact);
468
469 self.ref_match.map(|(mode, offset)| {
470 let prefix = match mode {
471 CompletionItemRefMode::Reference(Mutability::Shared) => "&",
472 CompletionItemRefMode::Reference(Mutability::Mut) => "&mut ",
473 CompletionItemRefMode::Dereference => "*",
474 };
475 let label = format!("{prefix}{}", self.label.primary);
476 (label, ide_db::text_edit::Indel::insert(offset, String::from(prefix)), relevance)
477 })
478 }
479}
480
481#[must_use]
483#[derive(Clone)]
484pub(crate) struct Builder {
485 source_range: TextRange,
486 imports_to_add: SmallVec<[LocatedImport; 1]>,
487 trait_name: Option<SmolStr>,
488 doc_aliases: Vec<SmolStr>,
489 label: SmolStr,
490 insert_text: Option<String>,
491 is_snippet: bool,
492 detail: Option<String>,
493 documentation: Option<Documentation<'static>>,
495 lookup: Option<SmolStr>,
496 kind: CompletionItemKind,
497 text_edit: Option<TextEdit>,
498 deprecated: bool,
499 trigger_call_info: bool,
500 relevance: CompletionRelevance,
501 ref_match: Option<(CompletionItemRefMode, TextSize)>,
502 edition: Edition,
503}
504
505impl Builder {
506 pub(crate) fn from_resolution(
507 ctx: &CompletionContext<'_>,
508 path_ctx: &PathCompletionCtx<'_>,
509 local_name: hir::Name,
510 resolution: hir::ScopeDef,
511 ) -> Self {
512 let doc_aliases = ctx.doc_aliases_in_scope(resolution);
513 render_path_resolution(
514 RenderContext::new(ctx).doc_aliases(doc_aliases),
515 path_ctx,
516 local_name,
517 resolution,
518 )
519 }
520
521 pub(crate) fn build(self, db: &RootDatabase) -> CompletionItem {
522 let _p = tracing::info_span!("item::Builder::build").entered();
523
524 let label = self.label;
525 let mut lookup = self.lookup.unwrap_or_else(|| label.clone());
526 let insert_text = self.insert_text.unwrap_or_else(|| label.to_string());
527
528 let mut detail_left = None;
529 if !self.doc_aliases.is_empty() {
530 let doc_aliases = self.doc_aliases.iter().join(", ");
531 detail_left = Some(format!("(alias {doc_aliases})"));
532 let lookup_doc_aliases = self
533 .doc_aliases
534 .iter()
535 .filter(|alias| {
539 let mut chars = alias.chars();
540 chars.next().is_some_and(char::is_alphabetic)
541 && chars.all(|c| c.is_alphanumeric() || c == '_')
542 })
543 .join("");
547 if !lookup_doc_aliases.is_empty() {
548 lookup = format_smolstr!("{lookup}{lookup_doc_aliases}");
549 }
550 }
551 if let [import_edit] = &*self.imports_to_add {
552 let detail_left = detail_left.get_or_insert_with(String::new);
554 format_to!(
555 detail_left,
556 "{}(use {})",
557 if detail_left.is_empty() { "" } else { " " },
558 import_edit.import_path.display(db, self.edition)
559 );
560 } else if let Some(trait_name) = self.trait_name {
561 let detail_left = detail_left.get_or_insert_with(String::new);
562 format_to!(
563 detail_left,
564 "{}(as {trait_name})",
565 if detail_left.is_empty() { "" } else { " " },
566 );
567 }
568
569 let text_edit = match self.text_edit {
570 Some(it) => it,
571 None => TextEdit::replace(self.source_range, insert_text),
572 };
573
574 let import_to_add = self
575 .imports_to_add
576 .into_iter()
577 .map(|import| import.import_path.display(db, self.edition).to_string())
578 .collect();
579
580 CompletionItem {
581 source_range: self.source_range,
582 label: CompletionItemLabel {
583 primary: label,
584 detail_left,
585 detail_right: self.detail.clone(),
586 },
587 text_edit,
588 is_snippet: self.is_snippet,
589 detail: self.detail,
590 documentation: self.documentation,
591 lookup,
592 kind: self.kind,
593 deprecated: self.deprecated,
594 trigger_call_info: self.trigger_call_info,
595 relevance: self.relevance,
596 ref_match: self.ref_match,
597 import_to_add,
598 }
599 }
600 pub(crate) fn lookup_by(&mut self, lookup: impl Into<SmolStr>) -> &mut Builder {
601 self.lookup = Some(lookup.into());
602 self
603 }
604 pub(crate) fn label(&mut self, label: impl Into<SmolStr>) -> &mut Builder {
605 self.label = label.into();
606 self
607 }
608 pub(crate) fn trait_name(&mut self, trait_name: SmolStr) -> &mut Builder {
609 self.trait_name = Some(trait_name);
610 self
611 }
612 pub(crate) fn doc_aliases(&mut self, doc_aliases: Vec<SmolStr>) -> &mut Builder {
613 self.doc_aliases = doc_aliases;
614 self
615 }
616 pub(crate) fn insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder {
617 self.insert_text = Some(insert_text.into());
618 self
619 }
620 pub(crate) fn insert_snippet(
621 &mut self,
622 cap: SnippetCap,
623 snippet: impl Into<String>,
624 ) -> &mut Builder {
625 let _ = cap;
626 self.is_snippet = true;
627 self.insert_text(snippet)
628 }
629 pub(crate) fn text_edit(&mut self, edit: TextEdit) -> &mut Builder {
630 self.text_edit = Some(edit);
631 self
632 }
633 pub(crate) fn snippet_edit(&mut self, _cap: SnippetCap, edit: TextEdit) -> &mut Builder {
634 self.is_snippet = true;
635 self.text_edit(edit)
636 }
637 pub(crate) fn detail(&mut self, detail: impl Into<String>) -> &mut Builder {
638 self.set_detail(Some(detail))
639 }
640 pub(crate) fn set_detail(&mut self, detail: Option<impl Into<String>>) -> &mut Builder {
641 self.detail = detail.map(Into::into);
642 if let Some(detail) = &self.detail
643 && never!(detail.contains('\n'), "multiline detail:\n{}", detail)
644 {
645 self.detail = Some(detail.split('\n').next().unwrap().to_owned());
646 }
647 self
648 }
649 #[allow(unused)]
650 pub(crate) fn documentation(&mut self, docs: Documentation<'_>) -> &mut Builder {
651 self.set_documentation(Some(docs))
652 }
653 pub(crate) fn set_documentation(&mut self, docs: Option<Documentation<'_>>) -> &mut Builder {
654 self.documentation = docs.map(Documentation::into_owned);
655 self
656 }
657 pub(crate) fn set_deprecated(&mut self, deprecated: bool) -> &mut Builder {
658 self.deprecated = deprecated;
659 self
660 }
661 pub(crate) fn set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder {
662 self.relevance = relevance;
663 self
664 }
665 pub(crate) fn with_relevance(
666 &mut self,
667 relevance: impl FnOnce(CompletionRelevance) -> CompletionRelevance,
668 ) -> &mut Builder {
669 self.relevance = relevance(mem::take(&mut self.relevance));
670 self
671 }
672 pub(crate) fn trigger_call_info(&mut self) -> &mut Builder {
673 self.trigger_call_info = true;
674 self
675 }
676 pub(crate) fn add_import(&mut self, import_to_add: LocatedImport) -> &mut Builder {
677 self.imports_to_add.push(import_to_add);
678 self
679 }
680 pub(crate) fn ref_match(
681 &mut self,
682 ref_mode: CompletionItemRefMode,
683 offset: TextSize,
684 ) -> &mut Builder {
685 self.ref_match = Some((ref_mode, offset));
686 self
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use itertools::Itertools;
693 use test_utils::assert_eq_text;
694
695 use super::{
696 CompletionRelevance, CompletionRelevancePostfixMatch, CompletionRelevanceTypeMatch,
697 };
698
699 fn check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>) {
709 let expected = format!("{expected_relevance_order:#?}");
710
711 let actual_relevance_order = expected_relevance_order
712 .into_iter()
713 .flatten()
714 .map(|r| (r.score(), r))
715 .sorted_by_key(|(score, _r)| *score)
716 .fold(
717 (u32::MIN, vec![vec![]]),
718 |(mut currently_collecting_score, mut out), (score, r)| {
719 if currently_collecting_score == score {
720 out.last_mut().unwrap().push(r);
721 } else {
722 currently_collecting_score = score;
723 out.push(vec![r]);
724 }
725 (currently_collecting_score, out)
726 },
727 )
728 .1;
729
730 let actual = format!("{actual_relevance_order:#?}");
731
732 assert_eq_text!(&expected, &actual);
733 }
734
735 #[test]
736 fn relevance_score() {
737 use CompletionRelevance as Cr;
738 let default = Cr::default();
739 let expected_relevance_order = vec![
742 vec![],
743 vec![Cr {
744 trait_: Some(crate::item::CompletionRelevanceTraitInfo {
745 notable_trait: false,
746 is_op_method: true,
747 }),
748 is_private_editable: true,
749 ..default
750 }],
751 vec![Cr {
752 trait_: Some(crate::item::CompletionRelevanceTraitInfo {
753 notable_trait: false,
754 is_op_method: true,
755 }),
756 ..default
757 }],
758 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::NonExact), ..default }],
759 vec![Cr { is_private_editable: true, ..default }],
760 vec![default],
761 vec![Cr { is_local: true, ..default }],
762 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::CouldUnify), ..default }],
763 vec![Cr { type_match: Some(CompletionRelevanceTypeMatch::Exact), ..default }],
764 vec![Cr { exact_name_match: true, ..default }],
765 vec![Cr { exact_name_match: true, is_local: true, ..default }],
766 vec![Cr {
767 exact_name_match: true,
768 type_match: Some(CompletionRelevanceTypeMatch::Exact),
769 ..default
770 }],
771 vec![Cr {
772 exact_name_match: true,
773 type_match: Some(CompletionRelevanceTypeMatch::Exact),
774 is_local: true,
775 ..default
776 }],
777 vec![Cr { postfix_match: Some(CompletionRelevancePostfixMatch::Exact), ..default }],
778 ];
779
780 check_relevance_score_ordered(expected_relevance_order);
781 }
782}