]> git.lizzy.rs Git - rust.git/blob - crates/ide_completion/src/item.rs
Introduce SymbolKind::Attribute
[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     Binding,
220     BuiltinType,
221     Keyword,
222     Method,
223     Snippet,
224     UnresolvedReference,
225 }
226
227 impl_from!(SymbolKind for CompletionItemKind);
228
229 impl CompletionItemKind {
230     #[cfg(test)]
231     pub(crate) fn tag(&self) -> &'static str {
232         match self {
233             CompletionItemKind::SymbolKind(kind) => match kind {
234                 SymbolKind::Attribute => "at",
235                 SymbolKind::BuiltinAttr => "ba",
236                 SymbolKind::Const => "ct",
237                 SymbolKind::ConstParam => "cp",
238                 SymbolKind::Enum => "en",
239                 SymbolKind::Field => "fd",
240                 SymbolKind::Function => "fn",
241                 SymbolKind::Impl => "im",
242                 SymbolKind::Label => "lb",
243                 SymbolKind::LifetimeParam => "lt",
244                 SymbolKind::Local => "lc",
245                 SymbolKind::Macro => "ma",
246                 SymbolKind::Module => "md",
247                 SymbolKind::SelfParam => "sp",
248                 SymbolKind::Static => "sc",
249                 SymbolKind::Struct => "st",
250                 SymbolKind::ToolModule => "tm",
251                 SymbolKind::Trait => "tt",
252                 SymbolKind::TypeAlias => "ta",
253                 SymbolKind::TypeParam => "tp",
254                 SymbolKind::Union => "un",
255                 SymbolKind::ValueParam => "vp",
256                 SymbolKind::Variant => "ev",
257             },
258             CompletionItemKind::Binding => "bn",
259             CompletionItemKind::BuiltinType => "bt",
260             CompletionItemKind::Keyword => "kw",
261             CompletionItemKind::Method => "me",
262             CompletionItemKind::Snippet => "sn",
263             CompletionItemKind::UnresolvedReference => "??",
264         }
265     }
266 }
267
268 impl CompletionItem {
269     pub(crate) fn new(
270         kind: impl Into<CompletionItemKind>,
271         source_range: TextRange,
272         label: impl Into<SmolStr>,
273     ) -> Builder {
274         let label = label.into();
275         Builder {
276             source_range,
277             label,
278             insert_text: None,
279             is_snippet: false,
280             trait_name: None,
281             detail: None,
282             documentation: None,
283             lookup: None,
284             kind: kind.into(),
285             text_edit: None,
286             deprecated: false,
287             trigger_call_info: None,
288             relevance: CompletionRelevance::default(),
289             ref_match: None,
290             imports_to_add: Default::default(),
291         }
292     }
293
294     /// What user sees in pop-up in the UI.
295     pub fn label(&self) -> &str {
296         &self.label
297     }
298     pub fn source_range(&self) -> TextRange {
299         self.source_range
300     }
301
302     pub fn text_edit(&self) -> &TextEdit {
303         &self.text_edit
304     }
305     /// Whether `text_edit` is a snippet (contains `$0` markers).
306     pub fn is_snippet(&self) -> bool {
307         self.is_snippet
308     }
309
310     /// Short one-line additional information, like a type
311     pub fn detail(&self) -> Option<&str> {
312         self.detail.as_deref()
313     }
314     /// A doc-comment
315     pub fn documentation(&self) -> Option<Documentation> {
316         self.documentation.clone()
317     }
318     /// What string is used for filtering.
319     pub fn lookup(&self) -> &str {
320         self.lookup.as_deref().unwrap_or(&self.label)
321     }
322
323     pub fn kind(&self) -> CompletionItemKind {
324         self.kind
325     }
326
327     pub fn deprecated(&self) -> bool {
328         self.deprecated
329     }
330
331     pub fn relevance(&self) -> CompletionRelevance {
332         self.relevance
333     }
334
335     pub fn trigger_call_info(&self) -> bool {
336         self.trigger_call_info
337     }
338
339     pub fn ref_match(&self) -> Option<(Mutability, CompletionRelevance)> {
340         // Relevance of the ref match should be the same as the original
341         // match, but with exact type match set because self.ref_match
342         // is only set if there is an exact type match.
343         let mut relevance = self.relevance;
344         relevance.type_match = Some(CompletionRelevanceTypeMatch::Exact);
345
346         self.ref_match.map(|mutability| (mutability, relevance))
347     }
348
349     pub fn imports_to_add(&self) -> &[ImportEdit] {
350         &self.import_to_add
351     }
352 }
353
354 /// An extra import to add after the completion is applied.
355 #[derive(Debug, Clone)]
356 pub struct ImportEdit {
357     pub import: LocatedImport,
358     pub scope: ImportScope,
359 }
360
361 impl ImportEdit {
362     /// Attempts to insert the import to the given scope, producing a text edit.
363     /// May return no edit in edge cases, such as scope already containing the import.
364     pub fn to_text_edit(&self, cfg: InsertUseConfig) -> Option<TextEdit> {
365         let _p = profile::span("ImportEdit::to_text_edit");
366
367         let new_ast = self.scope.clone_for_update();
368         insert_use::insert_use(&new_ast, mod_path_to_ast(&self.import.import_path), &cfg);
369         let mut import_insert = TextEdit::builder();
370         algo::diff(self.scope.as_syntax_node(), new_ast.as_syntax_node())
371             .into_text_edit(&mut import_insert);
372
373         Some(import_insert.finish())
374     }
375 }
376
377 /// A helper to make `CompletionItem`s.
378 #[must_use]
379 #[derive(Clone)]
380 pub(crate) struct Builder {
381     source_range: TextRange,
382     imports_to_add: SmallVec<[ImportEdit; 1]>,
383     trait_name: Option<SmolStr>,
384     label: SmolStr,
385     insert_text: Option<String>,
386     is_snippet: bool,
387     detail: Option<String>,
388     documentation: Option<Documentation>,
389     lookup: Option<SmolStr>,
390     kind: CompletionItemKind,
391     text_edit: Option<TextEdit>,
392     deprecated: bool,
393     trigger_call_info: Option<bool>,
394     relevance: CompletionRelevance,
395     ref_match: Option<Mutability>,
396 }
397
398 impl Builder {
399     pub(crate) fn build(self) -> CompletionItem {
400         let _p = profile::span("item::Builder::build");
401
402         let mut label = self.label;
403         let mut lookup = self.lookup;
404         let insert_text = self.insert_text.unwrap_or_else(|| label.to_string());
405
406         if let [import_edit] = &*self.imports_to_add {
407             // snippets can have multiple imports, but normal completions only have up to one
408             if let Some(original_path) = import_edit.import.original_path.as_ref() {
409                 lookup = lookup.or_else(|| Some(label.clone()));
410                 label = SmolStr::from(format!("{} (use {})", label, original_path));
411             }
412         } else if let Some(trait_name) = self.trait_name {
413             label = SmolStr::from(format!("{} (as {})", label, trait_name));
414         }
415
416         let text_edit = match self.text_edit {
417             Some(it) => it,
418             None => TextEdit::replace(self.source_range, insert_text),
419         };
420
421         CompletionItem {
422             source_range: self.source_range,
423             label,
424             text_edit,
425             is_snippet: self.is_snippet,
426             detail: self.detail,
427             documentation: self.documentation,
428             lookup,
429             kind: self.kind,
430             deprecated: self.deprecated,
431             trigger_call_info: self.trigger_call_info.unwrap_or(false),
432             relevance: self.relevance,
433             ref_match: self.ref_match,
434             import_to_add: self.imports_to_add,
435         }
436     }
437     pub(crate) fn lookup_by(&mut self, lookup: impl Into<SmolStr>) -> &mut Builder {
438         self.lookup = Some(lookup.into());
439         self
440     }
441     pub(crate) fn label(&mut self, label: impl Into<SmolStr>) -> &mut Builder {
442         self.label = label.into();
443         self
444     }
445     pub(crate) fn trait_name(&mut self, trait_name: SmolStr) -> &mut Builder {
446         self.trait_name = Some(trait_name);
447         self
448     }
449     pub(crate) fn insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder {
450         self.insert_text = Some(insert_text.into());
451         self
452     }
453     pub(crate) fn insert_snippet(
454         &mut self,
455         cap: SnippetCap,
456         snippet: impl Into<String>,
457     ) -> &mut Builder {
458         let _ = cap;
459         self.is_snippet = true;
460         self.insert_text(snippet)
461     }
462     pub(crate) fn text_edit(&mut self, edit: TextEdit) -> &mut Builder {
463         self.text_edit = Some(edit);
464         self
465     }
466     pub(crate) fn snippet_edit(&mut self, _cap: SnippetCap, edit: TextEdit) -> &mut Builder {
467         self.is_snippet = true;
468         self.text_edit(edit)
469     }
470     pub(crate) fn detail(&mut self, detail: impl Into<String>) -> &mut Builder {
471         self.set_detail(Some(detail))
472     }
473     pub(crate) fn set_detail(&mut self, detail: Option<impl Into<String>>) -> &mut Builder {
474         self.detail = detail.map(Into::into);
475         if let Some(detail) = &self.detail {
476             if never!(detail.contains('\n'), "multiline detail:\n{}", detail) {
477                 self.detail = Some(detail.splitn(2, '\n').next().unwrap().to_string());
478             }
479         }
480         self
481     }
482     #[allow(unused)]
483     pub(crate) fn documentation(&mut self, docs: Documentation) -> &mut Builder {
484         self.set_documentation(Some(docs))
485     }
486     pub(crate) fn set_documentation(&mut self, docs: Option<Documentation>) -> &mut Builder {
487         self.documentation = docs.map(Into::into);
488         self
489     }
490     pub(crate) fn set_deprecated(&mut self, deprecated: bool) -> &mut Builder {
491         self.deprecated = deprecated;
492         self
493     }
494     pub(crate) fn set_relevance(&mut self, relevance: CompletionRelevance) -> &mut Builder {
495         self.relevance = relevance;
496         self
497     }
498     pub(crate) fn trigger_call_info(&mut self) -> &mut Builder {
499         self.trigger_call_info = Some(true);
500         self
501     }
502     pub(crate) fn add_import(&mut self, import_to_add: ImportEdit) -> &mut Builder {
503         self.imports_to_add.push(import_to_add);
504         self
505     }
506     pub(crate) fn ref_match(&mut self, mutability: Mutability) -> &mut Builder {
507         self.ref_match = Some(mutability);
508         self
509     }
510 }
511
512 #[cfg(test)]
513 mod tests {
514     use itertools::Itertools;
515     use test_utils::assert_eq_text;
516
517     use super::{CompletionRelevance, CompletionRelevanceTypeMatch};
518
519     /// Check that these are CompletionRelevance are sorted in ascending order
520     /// by their relevance score.
521     ///
522     /// We want to avoid making assertions about the absolute score of any
523     /// item, but we do want to assert whether each is >, <, or == to the
524     /// others.
525     ///
526     /// If provided vec![vec![a], vec![b, c], vec![d]], then this will assert:
527     ///     a.score < b.score == c.score < d.score
528     fn check_relevance_score_ordered(expected_relevance_order: Vec<Vec<CompletionRelevance>>) {
529         let expected = format!("{:#?}", &expected_relevance_order);
530
531         let actual_relevance_order = expected_relevance_order
532             .into_iter()
533             .flatten()
534             .map(|r| (r.score(), r))
535             .sorted_by_key(|(score, _r)| *score)
536             .fold(
537                 (u32::MIN, vec![vec![]]),
538                 |(mut currently_collecting_score, mut out), (score, r)| {
539                     if currently_collecting_score == score {
540                         out.last_mut().unwrap().push(r);
541                     } else {
542                         currently_collecting_score = score;
543                         out.push(vec![r]);
544                     }
545                     (currently_collecting_score, out)
546                 },
547             )
548             .1;
549
550         let actual = format!("{:#?}", &actual_relevance_order);
551
552         assert_eq_text!(&expected, &actual);
553     }
554
555     #[test]
556     fn relevance_score() {
557         // This test asserts that the relevance score for these items is ascending, and
558         // that any items in the same vec have the same score.
559         let expected_relevance_order = vec![
560             vec![CompletionRelevance::default()],
561             vec![
562                 CompletionRelevance { exact_name_match: true, ..CompletionRelevance::default() },
563                 CompletionRelevance { is_local: true, ..CompletionRelevance::default() },
564             ],
565             vec![CompletionRelevance {
566                 exact_name_match: true,
567                 is_local: true,
568                 ..CompletionRelevance::default()
569             }],
570             vec![CompletionRelevance {
571                 type_match: Some(CompletionRelevanceTypeMatch::CouldUnify),
572                 ..CompletionRelevance::default()
573             }],
574             vec![CompletionRelevance {
575                 type_match: Some(CompletionRelevanceTypeMatch::Exact),
576                 ..CompletionRelevance::default()
577             }],
578             vec![CompletionRelevance {
579                 exact_name_match: true,
580                 type_match: Some(CompletionRelevanceTypeMatch::Exact),
581                 ..CompletionRelevance::default()
582             }],
583             vec![CompletionRelevance {
584                 exact_name_match: true,
585                 type_match: Some(CompletionRelevanceTypeMatch::Exact),
586                 is_local: true,
587                 ..CompletionRelevance::default()
588             }],
589             vec![CompletionRelevance {
590                 exact_name_match: false,
591                 type_match: None,
592                 is_local: false,
593                 exact_postfix_snippet_match: true,
594             }],
595         ];
596
597         check_relevance_score_ordered(expected_relevance_order);
598     }
599 }