diff options
Diffstat (limited to 'crates/ide/src/completion/completion_item.rs')
-rw-r--r-- | crates/ide/src/completion/completion_item.rs | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/crates/ide/src/completion/completion_item.rs b/crates/ide/src/completion/completion_item.rs new file mode 100644 index 000000000..9377cdc57 --- /dev/null +++ b/crates/ide/src/completion/completion_item.rs | |||
@@ -0,0 +1,384 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::fmt; | ||
4 | |||
5 | use hir::Documentation; | ||
6 | use syntax::TextRange; | ||
7 | use text_edit::TextEdit; | ||
8 | |||
9 | use crate::completion::completion_config::SnippetCap; | ||
10 | |||
11 | /// `CompletionItem` describes a single completion variant in the editor pop-up. | ||
12 | /// It is basically a POD with various properties. To construct a | ||
13 | /// `CompletionItem`, use `new` method and the `Builder` struct. | ||
14 | pub struct CompletionItem { | ||
15 | /// Used only internally in tests, to check only specific kind of | ||
16 | /// completion (postfix, keyword, reference, etc). | ||
17 | #[allow(unused)] | ||
18 | pub(crate) completion_kind: CompletionKind, | ||
19 | /// Label in the completion pop up which identifies completion. | ||
20 | label: String, | ||
21 | /// Range of identifier that is being completed. | ||
22 | /// | ||
23 | /// It should be used primarily for UI, but we also use this to convert | ||
24 | /// genetic TextEdit into LSP's completion edit (see conv.rs). | ||
25 | /// | ||
26 | /// `source_range` must contain the completion offset. `insert_text` should | ||
27 | /// start with what `source_range` points to, or VSCode will filter out the | ||
28 | /// completion silently. | ||
29 | source_range: TextRange, | ||
30 | /// What happens when user selects this item. | ||
31 | /// | ||
32 | /// Typically, replaces `source_range` with new identifier. | ||
33 | text_edit: TextEdit, | ||
34 | insert_text_format: InsertTextFormat, | ||
35 | |||
36 | /// What item (struct, function, etc) are we completing. | ||
37 | kind: Option<CompletionItemKind>, | ||
38 | |||
39 | /// Lookup is used to check if completion item indeed can complete current | ||
40 | /// ident. | ||
41 | /// | ||
42 | /// That is, in `foo.bar<|>` lookup of `abracadabra` will be accepted (it | ||
43 | /// contains `bar` sub sequence), and `quux` will rejected. | ||
44 | lookup: Option<String>, | ||
45 | |||
46 | /// Additional info to show in the UI pop up. | ||
47 | detail: Option<String>, | ||
48 | documentation: Option<Documentation>, | ||
49 | |||
50 | /// Whether this item is marked as deprecated | ||
51 | deprecated: bool, | ||
52 | |||
53 | /// If completing a function call, ask the editor to show parameter popup | ||
54 | /// after completion. | ||
55 | trigger_call_info: bool, | ||
56 | |||
57 | /// Score is useful to pre select or display in better order completion items | ||
58 | score: Option<CompletionScore>, | ||
59 | } | ||
60 | |||
61 | // We use custom debug for CompletionItem to make snapshot tests more readable. | ||
62 | impl fmt::Debug for CompletionItem { | ||
63 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
64 | let mut s = f.debug_struct("CompletionItem"); | ||
65 | s.field("label", &self.label()).field("source_range", &self.source_range()); | ||
66 | if self.text_edit().len() == 1 { | ||
67 | let atom = &self.text_edit().iter().next().unwrap(); | ||
68 | s.field("delete", &atom.delete); | ||
69 | s.field("insert", &atom.insert); | ||
70 | } else { | ||
71 | s.field("text_edit", &self.text_edit); | ||
72 | } | ||
73 | if let Some(kind) = self.kind().as_ref() { | ||
74 | s.field("kind", kind); | ||
75 | } | ||
76 | if self.lookup() != self.label() { | ||
77 | s.field("lookup", &self.lookup()); | ||
78 | } | ||
79 | if let Some(detail) = self.detail() { | ||
80 | s.field("detail", &detail); | ||
81 | } | ||
82 | if let Some(documentation) = self.documentation() { | ||
83 | s.field("documentation", &documentation); | ||
84 | } | ||
85 | if self.deprecated { | ||
86 | s.field("deprecated", &true); | ||
87 | } | ||
88 | if let Some(score) = &self.score { | ||
89 | s.field("score", score); | ||
90 | } | ||
91 | if self.trigger_call_info { | ||
92 | s.field("trigger_call_info", &true); | ||
93 | } | ||
94 | s.finish() | ||
95 | } | ||
96 | } | ||
97 | |||
98 | #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] | ||
99 | pub enum CompletionScore { | ||
100 | /// If only type match | ||
101 | TypeMatch, | ||
102 | /// If type and name match | ||
103 | TypeAndNameMatch, | ||
104 | } | ||
105 | |||
106 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
107 | pub enum CompletionItemKind { | ||
108 | Snippet, | ||
109 | Keyword, | ||
110 | Module, | ||
111 | Function, | ||
112 | BuiltinType, | ||
113 | Struct, | ||
114 | Enum, | ||
115 | EnumVariant, | ||
116 | Binding, | ||
117 | Field, | ||
118 | Static, | ||
119 | Const, | ||
120 | Trait, | ||
121 | TypeAlias, | ||
122 | Method, | ||
123 | TypeParam, | ||
124 | Macro, | ||
125 | Attribute, | ||
126 | UnresolvedReference, | ||
127 | } | ||
128 | |||
129 | impl CompletionItemKind { | ||
130 | #[cfg(test)] | ||
131 | pub(crate) fn tag(&self) -> &'static str { | ||
132 | match self { | ||
133 | CompletionItemKind::Attribute => "at", | ||
134 | CompletionItemKind::Binding => "bn", | ||
135 | CompletionItemKind::BuiltinType => "bt", | ||
136 | CompletionItemKind::Const => "ct", | ||
137 | CompletionItemKind::Enum => "en", | ||
138 | CompletionItemKind::EnumVariant => "ev", | ||
139 | CompletionItemKind::Field => "fd", | ||
140 | CompletionItemKind::Function => "fn", | ||
141 | CompletionItemKind::Keyword => "kw", | ||
142 | CompletionItemKind::Macro => "ma", | ||
143 | CompletionItemKind::Method => "me", | ||
144 | CompletionItemKind::Module => "md", | ||
145 | CompletionItemKind::Snippet => "sn", | ||
146 | CompletionItemKind::Static => "sc", | ||
147 | CompletionItemKind::Struct => "st", | ||
148 | CompletionItemKind::Trait => "tt", | ||
149 | CompletionItemKind::TypeAlias => "ta", | ||
150 | CompletionItemKind::TypeParam => "tp", | ||
151 | CompletionItemKind::UnresolvedReference => "??", | ||
152 | } | ||
153 | } | ||
154 | } | ||
155 | |||
156 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
157 | pub(crate) enum CompletionKind { | ||
158 | /// Parser-based keyword completion. | ||
159 | Keyword, | ||
160 | /// Your usual "complete all valid identifiers". | ||
161 | Reference, | ||
162 | /// "Secret sauce" completions. | ||
163 | Magic, | ||
164 | Snippet, | ||
165 | Postfix, | ||
166 | BuiltinType, | ||
167 | Attribute, | ||
168 | } | ||
169 | |||
170 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
171 | pub enum InsertTextFormat { | ||
172 | PlainText, | ||
173 | Snippet, | ||
174 | } | ||
175 | |||
176 | impl CompletionItem { | ||
177 | pub(crate) fn new( | ||
178 | completion_kind: CompletionKind, | ||
179 | source_range: TextRange, | ||
180 | label: impl Into<String>, | ||
181 | ) -> Builder { | ||
182 | let label = label.into(); | ||
183 | Builder { | ||
184 | source_range, | ||
185 | completion_kind, | ||
186 | label, | ||
187 | insert_text: None, | ||
188 | insert_text_format: InsertTextFormat::PlainText, | ||
189 | detail: None, | ||
190 | documentation: None, | ||
191 | lookup: None, | ||
192 | kind: None, | ||
193 | text_edit: None, | ||
194 | deprecated: None, | ||
195 | trigger_call_info: None, | ||
196 | score: None, | ||
197 | } | ||
198 | } | ||
199 | /// What user sees in pop-up in the UI. | ||
200 | pub fn label(&self) -> &str { | ||
201 | &self.label | ||
202 | } | ||
203 | pub fn source_range(&self) -> TextRange { | ||
204 | self.source_range | ||
205 | } | ||
206 | |||
207 | pub fn insert_text_format(&self) -> InsertTextFormat { | ||
208 | self.insert_text_format | ||
209 | } | ||
210 | |||
211 | pub fn text_edit(&self) -> &TextEdit { | ||
212 | &self.text_edit | ||
213 | } | ||
214 | |||
215 | /// Short one-line additional information, like a type | ||
216 | pub fn detail(&self) -> Option<&str> { | ||
217 | self.detail.as_deref() | ||
218 | } | ||
219 | /// A doc-comment | ||
220 | pub fn documentation(&self) -> Option<Documentation> { | ||
221 | self.documentation.clone() | ||
222 | } | ||
223 | /// What string is used for filtering. | ||
224 | pub fn lookup(&self) -> &str { | ||
225 | self.lookup.as_deref().unwrap_or(&self.label) | ||
226 | } | ||
227 | |||
228 | pub fn kind(&self) -> Option<CompletionItemKind> { | ||
229 | self.kind | ||
230 | } | ||
231 | |||
232 | pub fn deprecated(&self) -> bool { | ||
233 | self.deprecated | ||
234 | } | ||
235 | |||
236 | pub fn score(&self) -> Option<CompletionScore> { | ||
237 | self.score | ||
238 | } | ||
239 | |||
240 | pub fn trigger_call_info(&self) -> bool { | ||
241 | self.trigger_call_info | ||
242 | } | ||
243 | } | ||
244 | |||
245 | /// A helper to make `CompletionItem`s. | ||
246 | #[must_use] | ||
247 | pub(crate) struct Builder { | ||
248 | source_range: TextRange, | ||
249 | completion_kind: CompletionKind, | ||
250 | label: String, | ||
251 | insert_text: Option<String>, | ||
252 | insert_text_format: InsertTextFormat, | ||
253 | detail: Option<String>, | ||
254 | documentation: Option<Documentation>, | ||
255 | lookup: Option<String>, | ||
256 | kind: Option<CompletionItemKind>, | ||
257 | text_edit: Option<TextEdit>, | ||
258 | deprecated: Option<bool>, | ||
259 | trigger_call_info: Option<bool>, | ||
260 | score: Option<CompletionScore>, | ||
261 | } | ||
262 | |||
263 | impl Builder { | ||
264 | pub(crate) fn add_to(self, acc: &mut Completions) { | ||
265 | acc.add(self.build()) | ||
266 | } | ||
267 | |||
268 | pub(crate) fn build(self) -> CompletionItem { | ||
269 | let label = self.label; | ||
270 | let text_edit = match self.text_edit { | ||
271 | Some(it) => it, | ||
272 | None => TextEdit::replace( | ||
273 | self.source_range, | ||
274 | self.insert_text.unwrap_or_else(|| label.clone()), | ||
275 | ), | ||
276 | }; | ||
277 | |||
278 | CompletionItem { | ||
279 | source_range: self.source_range, | ||
280 | label, | ||
281 | insert_text_format: self.insert_text_format, | ||
282 | text_edit, | ||
283 | detail: self.detail, | ||
284 | documentation: self.documentation, | ||
285 | lookup: self.lookup, | ||
286 | kind: self.kind, | ||
287 | completion_kind: self.completion_kind, | ||
288 | deprecated: self.deprecated.unwrap_or(false), | ||
289 | trigger_call_info: self.trigger_call_info.unwrap_or(false), | ||
290 | score: self.score, | ||
291 | } | ||
292 | } | ||
293 | pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder { | ||
294 | self.lookup = Some(lookup.into()); | ||
295 | self | ||
296 | } | ||
297 | pub(crate) fn label(mut self, label: impl Into<String>) -> Builder { | ||
298 | self.label = label.into(); | ||
299 | self | ||
300 | } | ||
301 | pub(crate) fn insert_text(mut self, insert_text: impl Into<String>) -> Builder { | ||
302 | self.insert_text = Some(insert_text.into()); | ||
303 | self | ||
304 | } | ||
305 | pub(crate) fn insert_snippet( | ||
306 | mut self, | ||
307 | _cap: SnippetCap, | ||
308 | snippet: impl Into<String>, | ||
309 | ) -> Builder { | ||
310 | self.insert_text_format = InsertTextFormat::Snippet; | ||
311 | self.insert_text(snippet) | ||
312 | } | ||
313 | pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { | ||
314 | self.kind = Some(kind); | ||
315 | self | ||
316 | } | ||
317 | pub(crate) fn text_edit(mut self, edit: TextEdit) -> Builder { | ||
318 | self.text_edit = Some(edit); | ||
319 | self | ||
320 | } | ||
321 | pub(crate) fn snippet_edit(mut self, _cap: SnippetCap, edit: TextEdit) -> Builder { | ||
322 | self.insert_text_format = InsertTextFormat::Snippet; | ||
323 | self.text_edit(edit) | ||
324 | } | ||
325 | #[allow(unused)] | ||
326 | pub(crate) fn detail(self, detail: impl Into<String>) -> Builder { | ||
327 | self.set_detail(Some(detail)) | ||
328 | } | ||
329 | pub(crate) fn set_detail(mut self, detail: Option<impl Into<String>>) -> Builder { | ||
330 | self.detail = detail.map(Into::into); | ||
331 | self | ||
332 | } | ||
333 | #[allow(unused)] | ||
334 | pub(crate) fn documentation(self, docs: Documentation) -> Builder { | ||
335 | self.set_documentation(Some(docs)) | ||
336 | } | ||
337 | pub(crate) fn set_documentation(mut self, docs: Option<Documentation>) -> Builder { | ||
338 | self.documentation = docs.map(Into::into); | ||
339 | self | ||
340 | } | ||
341 | pub(crate) fn set_deprecated(mut self, deprecated: bool) -> Builder { | ||
342 | self.deprecated = Some(deprecated); | ||
343 | self | ||
344 | } | ||
345 | pub(crate) fn set_score(mut self, score: CompletionScore) -> Builder { | ||
346 | self.score = Some(score); | ||
347 | self | ||
348 | } | ||
349 | pub(crate) fn trigger_call_info(mut self) -> Builder { | ||
350 | self.trigger_call_info = Some(true); | ||
351 | self | ||
352 | } | ||
353 | } | ||
354 | |||
355 | impl<'a> Into<CompletionItem> for Builder { | ||
356 | fn into(self) -> CompletionItem { | ||
357 | self.build() | ||
358 | } | ||
359 | } | ||
360 | |||
361 | /// Represents an in-progress set of completions being built. | ||
362 | #[derive(Debug, Default)] | ||
363 | pub(crate) struct Completions { | ||
364 | buf: Vec<CompletionItem>, | ||
365 | } | ||
366 | |||
367 | impl Completions { | ||
368 | pub(crate) fn add(&mut self, item: impl Into<CompletionItem>) { | ||
369 | self.buf.push(item.into()) | ||
370 | } | ||
371 | pub(crate) fn add_all<I>(&mut self, items: I) | ||
372 | where | ||
373 | I: IntoIterator, | ||
374 | I::Item: Into<CompletionItem>, | ||
375 | { | ||
376 | items.into_iter().for_each(|item| self.add(item.into())) | ||
377 | } | ||
378 | } | ||
379 | |||
380 | impl Into<Vec<CompletionItem>> for Completions { | ||
381 | fn into(self) -> Vec<CompletionItem> { | ||
382 | self.buf | ||
383 | } | ||
384 | } | ||