]> git.lizzy.rs Git - rust.git/blob - crates/ide_completion/src/item.rs
Replace some String usages with SmolStr in completions
[rust.git] / crates / ide_completion / src / item.rs
1 //! See `CompletionItem` structure.
2
3 use std::fmt;
4
5 use hir::{Documentation, Mutability};
6 use ide_db::{
7     helpers::{
8         import_assets::LocatedImport,
9         insert_use::{self, ImportScope, InsertUseConfig},
10         mod_path_to_ast, SnippetCap,
11     },
12     SymbolKind,
13 };
14 use smallvec::SmallVec;
15 use stdx::{impl_from, never};
16 use syntax::{algo, SmolStr, TextRange};
17 use text_edit::TextEdit;
18
19 /// `CompletionItem` describes a single completion variant in the editor pop-up.
20 /// It is basically a POD with various properties. To construct a
21 /// `CompletionItem`, use `new` method and the `Builder` struct.
22 #[derive(Clone)]
23 pub struct CompletionItem {
24     /// Label in the completion pop up which identifies completion.
25     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. `insert_text` should
32     /// start with what `source_range` points to, or VSCode will filter out the
33     /// completion silently.
34     source_range: TextRange,
35     /// What happens when user selects this item.
36     ///
37     /// Typically, replaces `source_range` with new identifier.
38     text_edit: TextEdit,
39     is_snippet: bool,
40
41     /// What item (struct, function, etc) are we completing.
42     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     lookup: Option<SmolStr>,
50
51     /// Additional info to show in the UI pop up.
52     detail: Option<String>,
53     documentation: Option<Documentation>,
54
55     /// Whether this item is marked as deprecated
56     deprecated: bool,
57
58     /// If completing a function call, ask the editor to show parameter popup
59     /// after completion.
60     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     relevance: CompletionRelevance,
70
71     /// Indicates that a reference or mutable reference to this variable is a
72     /// possible match.
73     ref_match: Option<Mutability>,
74
75     /// The import data to add to completion's edits.
76     import_to_add: SmallVec<[ImportEdit; 1]>,
77 }
78
79 // We use custom debug for CompletionItem to make snapshot tests more readable.
80 impl fmt::Debug for CompletionItem {
81     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82         let mut s = f.debug_struct("CompletionItem");
83         s.field("label", &self.label()).field("source_range", &self.source_range());
84         if self.text_edit().len() == 1 {
85             let atom = &self.text_edit().iter().next().unwrap();
86             s.field("delete", &atom.delete);
87             s.field("insert", &atom.insert);
88         } else {
89             s.field("text_edit", &self.text_edit);
90         }
91         s.field("kind", &self.kind());
92         if self.lookup() != self.label() {
93             s.field("lookup", &self.lookup());
94         }
95         if let Some(detail) = self.detail() {
96             s.field("detail", &detail);
97         }
98         if let Some(documentation) = self.documentation() {
99             s.field("documentation", &documentation);
100         }
101         if self.deprecated {
102             s.field("deprecated", &true);
103         }
104
105         if self.relevance != CompletionRelevance::default() {
106             s.field("relevance", &self.relevance);
107         }
108
109         if let Some(mutability) = &self.ref_match {
110             s.field("ref_match", &format!("&{}", mutability.as_keyword_for_ref()));
111         }
112         if self.trigger_call_info {
113             s.field("trigger_call_info", &true);
114         }
115         s.finish()
116     }
117 }
118
119 #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
120 pub struct CompletionRelevance {
121     /// This is set in cases like these:
122     ///
123     /// ```
124     /// fn f(spam: String) {}
125     /// fn main {
126     ///     let spam = 92;
127     ///     f($0) // name of local matches the name of param
128     /// }
129     /// ```
130     pub exact_name_match: bool,
131     /// See CompletionRelevanceTypeMatch doc comments for cases where this is set.
132     pub type_match: Option<CompletionRelevanceTypeMatch>,
133     /// This is set in cases like these:
134     ///
135     /// ```
136     /// fn foo(a: u32) {
137     ///     let b = 0;
138     ///     $0 // `a` and `b` are local
139     /// }
140     /// ```
141     pub is_local: bool,
142     /// This is set in cases like these:
143     ///
144     /// ```
145     /// (a > b).not$0
146     /// ```
147     ///
148     /// Basically, we want to guarantee that postfix snippets always takes
149     /// precedence over everything else.
150     pub exact_postfix_snippet_match: bool,
151 }
152
153 #[derive(Debug, Clone, Copy, Eq, PartialEq)]
154 pub enum CompletionRelevanceTypeMatch {
155     /// This is set in cases like these:
156     ///
157     /// ```
158     /// enum Option<T> { Some(T), None }
159     /// fn f(a: Option<u32>) {}
160     /// fn main {
161     ///     f(Option::N$0) // type `Option<T>` could unify with `Option<u32>`
162     /// }
163     /// ```
164     CouldUnify,
165     /// This is set in cases like these:
166     ///
167     /// ```
168     /// fn f(spam: String) {}
169     /// fn main {
170     ///     let foo = String::new();
171     ///     f($0) // type of local matches the type of param
172     /// }
173     /// ```
174     Exact,
175 }
176
177 impl CompletionRelevance {
178     /// Provides a relevance score. Higher values are more relevant.
179     ///
180     /// The absolute value of the relevance score is not meaningful, for
181     /// example a value of 0 doesn't mean "not relevant", rather
182     /// it means "least relevant". The score value should only be used
183     /// for relative ordering.
184     ///
185     /// See is_relevant if you need to make some judgement about score
186     /// in an absolute sense.
187     pub fn score(&self) -> u32 {
188         let mut score = 0;
189
190         if self.exact_name_match {
191             score += 1;
192         }
193         score += match self.type_match {
194             Some(CompletionRelevanceTypeMatch::Exact) => 4,
195             Some(CompletionRelevanceTypeMatch::CouldUnify) => 3,
196             None => 0,
197         };
198         if self.is_local {
199             score += 1;
200         }
201         if self.exact_postfix_snippet_match {
202             score += 100;
203         }
204         score
205     }
206
207     /// Returns true when the score for this threshold is above
208     /// some threshold such that we think it is especially likely
209     /// to be relevant.
210     pub fn is_relevant(&self) -> bool {
211         self.score() > 0
212     }
213 }
214
215 /// The type of the completion item.
216 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
217 pub enum CompletionItemKind {
218     SymbolKind(SymbolKind),
219     Attribute,
220     Binding,
221     BuiltinType,
222     Keyword,
223     Method,
224     Snippet,
225     UnresolvedReference,
226 }
227
228 impl_from!(SymbolKind for CompletionItemKind);
229
230 impl CompletionItemKind {
231     #[cfg(test)]
232     pub(crate) fn tag(&self) -> &'static str {
233         match self {
234             CompletionItemKind::SymbolKind(kind) => match kind {
235                 SymbolKind::Const => "ct",
236                 SymbolKind::ConstParam => "cp",
237                 SymbolKind::Enum => "en",
238                 SymbolKind::Field => "fd",
239                 SymbolKind::Function => "fn",
240                 SymbolKind::Impl => "im",
241                 SymbolKind::Label => "lb",
242                 SymbolKind::LifetimeParam => "lt",
243                 SymbolKind::Local => "lc",
244                 SymbolKind::Macro => "ma",
245                 SymbolKind::Module => "md",
246                 SymbolKind::SelfParam => "sp",
247                 SymbolKind::Static => "sc",
248                 SymbolKind::Struct => "st",
249                 SymbolKind::Trait => "tt",
250                 SymbolKind::TypeAlias => "ta",
251                 SymbolKind::TypeParam => "tp",
252                 SymbolKind::Union => "un",
253                 SymbolKind::ValueParam => "vp",
254                 SymbolKind::Variant => "ev",
255             },
256             CompletionItemKind::Attribute => "at",
257             CompletionItemKind::Binding => "bn",
258             CompletionItemKind::BuiltinType => "bt",
259             CompletionItemKind::Keyword => "kw",
260             CompletionItemKind::Method => "me",
261             CompletionItemKind::Snippet => "sn",
262             CompletionItemKind::UnresolvedReference => "??",
263         }
264     }
265 }
266
267 impl CompletionItem {
268     pub(crate) fn new(
269         kind: impl Into<CompletionItemKind>,
270         source_range: TextRange,
271         label: impl Into<SmolStr>,
272     ) -> Builder {
273         let label = label.into();
274         Builder {
275             source_range,
276             label,
277             insert_text: None,
278             is_snippet: false,
279             trait_name: None,
280             detail: None,
281             documentation: None,
282             lookup: None,
283             kind: kind.into(),
284             text_edit: None,
285             deprecated: false,
286             trigger_call_info: None,
287             relevance: CompletionRelevance::default(),
288             ref_match: None,
289             imports_to_add: Default::default(),
290         }
291     }
292
293     /// What user sees in pop-up in the UI.
294     pub fn label(&self) -> &str {
295         &self.label
296     }
297     pub fn source_range(&self) -> TextRange {
298         self.source_range
299     }
300
301     pub fn text_edit(&self) -> &TextEdit {
302         &self.text_edit
303     }
304     /// Whether `text_edit` is a snippet (contains `$0` markers).
305     pub fn is_snippet(&self) -> bool {
306         self.is_snippet
307     }
308
309     /// Short one-line additional information, like a type
310     pub fn detail(&self) -> Option<&str> {
311         self.detail.as_deref()
312     }
313     /// A doc-comment
314     pub fn documentation(&self) -> Option<Documentation> {
315         self.documentation.clone()
316     }
317     /// What string is used for filtering.
318     pub fn lookup(&self) -> &str {
319         self.lookup.as_deref().unwrap_or(&self.label)
320     }
321
322     pub fn kind(&self) -> CompletionItemKind {
323         self.kind
324     }
325
326     pub fn deprecated(&self) -> bool {
327         self.deprecated
328     }
329
330     pub fn relevance(&self) -> CompletionRelevance {
331         self.relevance
332     }
333
334     pub fn trigger_call_info(&self) -> bool {
335         self.trigger_call_info
336     }
337
338     pub fn ref_match(&self) -> Option<(Mutability, CompletionRelevance)> {
339         // Relevance of the ref match should be the same as the original
340         // match, but with exact type match set because self.ref_match
341         // is only set if there is an exact type match.
342         let mut relevance = self.relevance;
343         relevance.type_match = Some(CompletionRelevanceTypeMatch::Exact);
344
345         self.ref_match.map(|mutability| (mutability, relevance))
346     }
347
348     pub fn imports_to_add(&self) -> &[ImportEdit] {
349         &self.import_to_add
350     }
351 }
352
353 /// An extra import to add after the completion is applied.
354 #[derive(Debug, Clone)]
355 pub struct ImportEdit {
356     pub import: LocatedImport,
357     pub scope: ImportScope,
358 }
359
360 impl ImportEdit {
361     /// Attempts to insert the import to the given scope, producing a text edit.
362     /// May return no edit in edge cases, such as scope already containing the import.
363     pub fn to_text_edit(&self, cfg: InsertUseConfig) -> Option<TextEdit> {
364         let _p = profile::span("ImportEdit::to_text_edit");
365
366         let new_ast = self.scope.clone_for_update();
367         insert_use::insert_use(&new_ast, mod_path_to_ast(&self.import.import_path), &cfg);
368         let mut import_insert = TextEdit::builder();
369         algo::diff(self.scope.as_syntax_node(), new_ast.as_syntax_node())
370             .into_text_edit(&mut import_insert);
371
372         Some(import_insert.finish())
373     }
374 }
375
376 /// A helper to make `CompletionItem`s.
377 #[must_use]
378 #[derive(Clone)]
379 pub(crate) struct Builder {
380     source_range: TextRange,
381     imports_to_add: SmallVec<[ImportEdit; 1]>,
382     trait_name: Option<SmolStr>,
383     label: SmolStr,
384     insert_text: Option<String>,
385     is_snippet: bool,
386     detail: Option<String>,
387     documentation: Option<Documentation>,
388     lookup: Option<SmolStr>,
389     kind: CompletionItemKind,
390     text_edit: Option<TextEdit>,
391     deprecated: bool,
392     trigger_call_info: Option<bool>,
393     relevance: CompletionRelevance,
394     ref_match: Option<Mutability>,
395 }
396
397 impl Builder {
398     pub(crate) fn build(self) -> CompletionItem {
399         let _p = profile::span("item::Builder::build");
400
401         let mut label = self.label;
402         let mut lookup = self.lookup;
403         let insert_text = self.insert_text.unwrap_or_else(|| label.to_string());
404
405         if let [import_edit] = &*self.imports_to_add {
406             // snippets can have multiple imports, but normal completions only have up to one
407             if let Some(original_path) = import_edit.import.original_path.as_ref() {
408                 lookup = lookup.or_else(|| Some(label.clone()));
409                 label = SmolStr::from(format!("{} (use {})", label, original_path));
410             }
411         } else if let Some(trait_name) = self.trait_name {
412             label = SmolStr::from(format!("{} (as {})", label, trait_name));
413         }
414
415         let text_edit = match self.text_edit {
416             Some(it) => it,
417             None => TextEdit::replace(self.source_range, insert_text),
418         };
419
420         CompletionItem {
421             source_range: self.source_range,
422             label,
423             text_edit,
424             is_snippet: self.is_snippet,
425             detail: self.detail,
426             documentation: self.documentation,
427             lookup,
428             kind: self.kind,
429             deprecated: self.deprecated,
430             trigger_call_info: self.trigger_call_info.unwrap_or(false),
431             relevance: self.relevance,
432             ref_match: self.ref_match,
433             import_to_add: self.imports_to_add,
434         }
435     }
436     pub(crate) fn lookup_by(&mut self, lookup: impl Into<SmolStr>) -> &mut Builder {
437         self.lookup = Some(lookup.into());
438         self
439     }
440     pub(crate) fn label(&mut self, label: impl Into<SmolStr>) -> &mut Builder {
441         self.label = label.into();
442         self
443     }
444     pub(crate) fn trait_name(&mut self, trait_name: SmolStr) -> &mut Builder {
445         self.trait_name = Some(trait_name);
446         self
447     }
448     pub(crate) fn insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder {
449         self.insert_text = Some(insert_text.into());
450         self
451     }
452     pub(crate) fn insert_snippet(
453         &mut self,
454         cap: SnippetCap,
455         snippet: impl Into<String>,
456     ) -> &mut Builder {
457         let _ = cap;
458         self.is_snippet = true;
459         self.insert_text(snippet)
460     }
461     pub(crate) fn text_edit(&mut self, edit: TextEdit) -> &mut Builder {
462         self.text_edit = Some(edit);
463         self
464     }
465     pub(crate) fn snippet_edit(&mut self, _cap: SnippetCap, edit: TextEdit) -> &mut Builder {
466         self.is_snippet = true;
467         self.text_edit(edit)
468     }
469     pub(crate) fn detail(&mut self, detail: impl Into<String>) -> &mut Builder {
470         self.set_detail(Some(detail))
471     }
472     pub(crate) fn set_detail(&mut self, detail: Option<impl Into<String>>) -> &mut Builder {
473         self.detail = detail.map(Into::into);
474         if let Some(detail) = &self.detail {
475             if never!(detail.contains('\n'), "multiline detail:\n{}", detail) {
476                 self.detail = Some(detail.splitn(2, '\n').next().unwrap().to_string());
477             }
478         }
479         self
480     }
481     #[allow(unused)]
482     pub(crate) fn documentation(&mut self, docs: Documentation) -> &mut Builder {
483         self.set_documentation(Some(docs))
484     }
485     pub(crate) fn set_documentation(&mut self, docs: Option<Documentation>) -> &mut Builder {
486         self.documentation = docs.map(Into::into);
487         self
488     }
489     pub(crate) fn set_deprecated(&mut self, deprecated: bool) -> &mut Builder {
490         self.deprecated = deprecated;
491         self
492     }
493     pub(crate) fn set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder {
494         self.relevance = relevance;
495         self
496     }
497     pub(crate) fn trigger_call_info(&mut self) -> &mut Builder {
498         self.trigger_call_info = Some(true);
499         self
500     }
501     pub(crate) fn add_import(&mut self, import_to_add: ImportEdit) -> &mut Builder {
502         self.imports_to_add.push(import_to_add);
503         self
504     }
505     pub(crate) fn ref_match(&mut self, mutability: Mutability) -> &mut Builder {
506         self.ref_match = Some(mutability);
507         self
508     }
509 }
510
511 #[cfg(test)]
512 mod tests {
513     use itertools::Itertools;
514     use test_utils::assert_eq_text;
515
516     use super::{CompletionRelevance, CompletionRelevanceTypeMatch};
517
518     /// Check that these are CompletionRelevance are sorted in ascending order
519     /// by their relevance score.
520     ///
521     /// We want to avoid making assertions about the absolute score of any
522     /// item, but we do want to assert whether each is >, <, or == to the
523     /// others.
524     ///
525     /// If provided vec![vec![a], vec![b, c], vec![d]], then this will assert:
526     ///     a.score < b.score == c.score < d.score
527     fn check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>) {
528         let expected = format!("{:#?}", &expected_relevance_order);
529
530         let actual_relevance_order = expected_relevance_order
531             .into_iter()
532             .flatten()
533             .map(|r| (r.score(), r))
534             .sorted_by_key(|(score, _r)| *score)
535             .fold(
536                 (u32::MIN, vec![vec![]]),
537                 |(mut currently_collecting_score, mut out), (score, r)| {
538                     if currently_collecting_score == score {
539                         out.last_mut().unwrap().push(r);
540                     } else {
541                         currently_collecting_score = score;
542                         out.push(vec![r]);
543                     }
544                     (currently_collecting_score, out)
545                 },
546             )
547             .1;
548
549         let actual = format!("{:#?}", &actual_relevance_order);
550
551         assert_eq_text!(&expected, &actual);
552     }
553
554     #[test]
555     fn relevance_score() {
556         // This test asserts that the relevance score for these items is ascending, and
557         // that any items in the same vec have the same score.
558         let expected_relevance_order = vec![
559             vec![CompletionRelevance::default()],
560             vec![
561                 CompletionRelevance { exact_name_match: true, ..CompletionRelevance::default() },
562                 CompletionRelevance { is_local: true, ..CompletionRelevance::default() },
563             ],
564             vec![CompletionRelevance {
565                 exact_name_match: true,
566                 is_local: true,
567                 ..CompletionRelevance::default()
568             }],
569             vec![CompletionRelevance {
570                 type_match: Some(CompletionRelevanceTypeMatch::CouldUnify),
571                 ..CompletionRelevance::default()
572             }],
573             vec![CompletionRelevance {
574                 type_match: Some(CompletionRelevanceTypeMatch::Exact),
575                 ..CompletionRelevance::default()
576             }],
577             vec![CompletionRelevance {
578                 exact_name_match: true,
579                 type_match: Some(CompletionRelevanceTypeMatch::Exact),
580                 ..CompletionRelevance::default()
581             }],
582             vec![CompletionRelevance {
583                 exact_name_match: true,
584                 type_match: Some(CompletionRelevanceTypeMatch::Exact),
585                 is_local: true,
586                 ..CompletionRelevance::default()
587             }],
588             vec![CompletionRelevance {
589                 exact_name_match: false,
590                 type_match: None,
591                 is_local: false,
592                 exact_postfix_snippet_match: true,
593             }],
594         ];
595
596         check_relevance_score_ordered(expected_relevance_order);
597     }
598 }