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