diff options
Diffstat (limited to 'crates/ra_ide_api/src/completion/completion_item.rs')
-rw-r--r-- | crates/ra_ide_api/src/completion/completion_item.rs | 210 |
1 files changed, 122 insertions, 88 deletions
diff --git a/crates/ra_ide_api/src/completion/completion_item.rs b/crates/ra_ide_api/src/completion/completion_item.rs index 11d00f78c..7bd634498 100644 --- a/crates/ra_ide_api/src/completion/completion_item.rs +++ b/crates/ra_ide_api/src/completion/completion_item.rs | |||
@@ -1,6 +1,10 @@ | |||
1 | use hir::PerNs; | 1 | use hir::PerNs; |
2 | use ra_text_edit::{ | ||
3 | AtomTextEdit, | ||
4 | TextEdit, | ||
5 | }; | ||
2 | 6 | ||
3 | use crate::completion::CompletionContext; | 7 | use crate::completion::completion_context::CompletionContext; |
4 | 8 | ||
5 | /// `CompletionItem` describes a single completion variant in the editor pop-up. | 9 | /// `CompletionItem` describes a single completion variant in the editor pop-up. |
6 | /// It is basically a POD with various properties. To construct a | 10 | /// It is basically a POD with various properties. To construct a |
@@ -11,15 +15,29 @@ pub struct CompletionItem { | |||
11 | /// completion. | 15 | /// completion. |
12 | completion_kind: CompletionKind, | 16 | completion_kind: CompletionKind, |
13 | label: String, | 17 | label: String, |
18 | kind: Option<CompletionItemKind>, | ||
14 | detail: Option<String>, | 19 | detail: Option<String>, |
15 | lookup: Option<String>, | 20 | lookup: Option<String>, |
16 | snippet: Option<String>, | 21 | /// The format of the insert text. The format applies to both the `insert_text` property |
17 | kind: Option<CompletionItemKind>, | 22 | /// and the `insert` property of a provided `text_edit`. |
18 | } | 23 | insert_text_format: InsertTextFormat, |
19 | 24 | /// An edit which is applied to a document when selecting this completion. When an edit is | |
20 | pub enum InsertText { | 25 | /// provided the value of `insert_text` is ignored. |
21 | PlainText { text: String }, | 26 | /// |
22 | Snippet { text: String }, | 27 | /// *Note:* The range of the edit must be a single line range and it must contain the position |
28 | /// at which completion has been requested. | ||
29 | /// | ||
30 | /// *Note:* If sending a range that overlaps a string, the string should match the relevant | ||
31 | /// part of the replacement text, or be filtered out. | ||
32 | text_edit: Option<AtomTextEdit>, | ||
33 | /// An optional array of additional text edits that are applied when | ||
34 | /// selecting this completion. Edits must not overlap (including the same insert position) | ||
35 | /// with the main edit nor with themselves. | ||
36 | /// | ||
37 | /// Additional text edits should be used to change text unrelated to the current cursor position | ||
38 | /// (for example adding an import statement at the top of the file if the completion item will | ||
39 | /// insert an unqualified type). | ||
40 | additional_text_edits: Option<TextEdit>, | ||
23 | } | 41 | } |
24 | 42 | ||
25 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
@@ -40,7 +58,7 @@ pub enum CompletionItemKind { | |||
40 | Method, | 58 | Method, |
41 | } | 59 | } |
42 | 60 | ||
43 | #[derive(Debug, PartialEq, Eq)] | 61 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] |
44 | pub(crate) enum CompletionKind { | 62 | pub(crate) enum CompletionKind { |
45 | /// Parser-based keyword completion. | 63 | /// Parser-based keyword completion. |
46 | Keyword, | 64 | Keyword, |
@@ -51,16 +69,30 @@ pub(crate) enum CompletionKind { | |||
51 | Snippet, | 69 | Snippet, |
52 | } | 70 | } |
53 | 71 | ||
72 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
73 | pub enum InsertTextFormat { | ||
74 | PlainText, | ||
75 | Snippet, | ||
76 | } | ||
77 | |||
54 | impl CompletionItem { | 78 | impl CompletionItem { |
55 | pub(crate) fn new(completion_kind: CompletionKind, label: impl Into<String>) -> Builder { | 79 | pub(crate) fn new<'a>( |
80 | completion_kind: CompletionKind, | ||
81 | ctx: &'a CompletionContext, | ||
82 | label: impl Into<String>, | ||
83 | ) -> Builder<'a> { | ||
56 | let label = label.into(); | 84 | let label = label.into(); |
57 | Builder { | 85 | Builder { |
86 | ctx, | ||
58 | completion_kind, | 87 | completion_kind, |
59 | label, | 88 | label, |
89 | insert_text: None, | ||
90 | insert_text_format: InsertTextFormat::PlainText, | ||
60 | detail: None, | 91 | detail: None, |
61 | lookup: None, | 92 | lookup: None, |
62 | snippet: None, | ||
63 | kind: None, | 93 | kind: None, |
94 | text_edit: None, | ||
95 | additional_text_edits: None, | ||
64 | } | 96 | } |
65 | } | 97 | } |
66 | /// What user sees in pop-up in the UI. | 98 | /// What user sees in pop-up in the UI. |
@@ -78,64 +110,100 @@ impl CompletionItem { | |||
78 | .map(|it| it.as_str()) | 110 | .map(|it| it.as_str()) |
79 | .unwrap_or(self.label()) | 111 | .unwrap_or(self.label()) |
80 | } | 112 | } |
81 | /// What is inserted. | 113 | |
82 | pub fn insert_text(&self) -> InsertText { | 114 | pub fn insert_text_format(&self) -> InsertTextFormat { |
83 | match &self.snippet { | 115 | self.insert_text_format.clone() |
84 | None => InsertText::PlainText { | ||
85 | text: self.label.clone(), | ||
86 | }, | ||
87 | Some(it) => InsertText::Snippet { text: it.clone() }, | ||
88 | } | ||
89 | } | 116 | } |
90 | 117 | ||
91 | pub fn kind(&self) -> Option<CompletionItemKind> { | 118 | pub fn kind(&self) -> Option<CompletionItemKind> { |
92 | self.kind | 119 | self.kind |
93 | } | 120 | } |
121 | pub fn text_edit(&mut self) -> Option<&AtomTextEdit> { | ||
122 | self.text_edit.as_ref() | ||
123 | } | ||
124 | pub fn take_additional_text_edits(&mut self) -> Option<TextEdit> { | ||
125 | self.additional_text_edits.take() | ||
126 | } | ||
94 | } | 127 | } |
95 | 128 | ||
96 | /// A helper to make `CompletionItem`s. | 129 | /// A helper to make `CompletionItem`s. |
97 | #[must_use] | 130 | #[must_use] |
98 | pub(crate) struct Builder { | 131 | pub(crate) struct Builder<'a> { |
132 | ctx: &'a CompletionContext<'a>, | ||
99 | completion_kind: CompletionKind, | 133 | completion_kind: CompletionKind, |
100 | label: String, | 134 | label: String, |
135 | insert_text: Option<String>, | ||
136 | insert_text_format: InsertTextFormat, | ||
101 | detail: Option<String>, | 137 | detail: Option<String>, |
102 | lookup: Option<String>, | 138 | lookup: Option<String>, |
103 | snippet: Option<String>, | ||
104 | kind: Option<CompletionItemKind>, | 139 | kind: Option<CompletionItemKind>, |
140 | text_edit: Option<AtomTextEdit>, | ||
141 | additional_text_edits: Option<TextEdit>, | ||
105 | } | 142 | } |
106 | 143 | ||
107 | impl Builder { | 144 | impl<'a> Builder<'a> { |
108 | pub(crate) fn add_to(self, acc: &mut Completions) { | 145 | pub(crate) fn add_to(self, acc: &mut Completions) { |
109 | acc.add(self.build()) | 146 | acc.add(self.build()) |
110 | } | 147 | } |
111 | 148 | ||
112 | pub(crate) fn build(self) -> CompletionItem { | 149 | pub(crate) fn build(self) -> CompletionItem { |
150 | let self_text_edit = self.text_edit; | ||
151 | let self_insert_text = self.insert_text; | ||
152 | let text_edit = match (self_text_edit, self_insert_text) { | ||
153 | (Some(text_edit), ..) => Some(text_edit), | ||
154 | (None, Some(insert_text)) => { | ||
155 | Some(AtomTextEdit::replace(self.ctx.leaf_range(), insert_text)) | ||
156 | } | ||
157 | _ => None, | ||
158 | }; | ||
159 | |||
113 | CompletionItem { | 160 | CompletionItem { |
114 | label: self.label, | 161 | label: self.label, |
115 | detail: self.detail, | 162 | detail: self.detail, |
163 | insert_text_format: self.insert_text_format, | ||
116 | lookup: self.lookup, | 164 | lookup: self.lookup, |
117 | snippet: self.snippet, | ||
118 | kind: self.kind, | 165 | kind: self.kind, |
119 | completion_kind: self.completion_kind, | 166 | completion_kind: self.completion_kind, |
167 | text_edit, | ||
168 | additional_text_edits: self.additional_text_edits, | ||
120 | } | 169 | } |
121 | } | 170 | } |
122 | pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder { | 171 | pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder<'a> { |
123 | self.lookup = Some(lookup.into()); | 172 | self.lookup = Some(lookup.into()); |
124 | self | 173 | self |
125 | } | 174 | } |
126 | pub(crate) fn snippet(mut self, snippet: impl Into<String>) -> Builder { | 175 | pub(crate) fn insert_text(mut self, insert_text: impl Into<String>) -> Builder<'a> { |
127 | self.snippet = Some(snippet.into()); | 176 | self.insert_text = Some(insert_text.into()); |
177 | self | ||
178 | } | ||
179 | pub(crate) fn insert_text_format( | ||
180 | mut self, | ||
181 | insert_text_format: InsertTextFormat, | ||
182 | ) -> Builder<'a> { | ||
183 | self.insert_text_format = insert_text_format; | ||
128 | self | 184 | self |
129 | } | 185 | } |
130 | pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { | 186 | pub(crate) fn snippet(mut self, snippet: impl Into<String>) -> Builder<'a> { |
187 | self.insert_text_format = InsertTextFormat::Snippet; | ||
188 | self.insert_text(snippet) | ||
189 | } | ||
190 | pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder<'a> { | ||
131 | self.kind = Some(kind); | 191 | self.kind = Some(kind); |
132 | self | 192 | self |
133 | } | 193 | } |
194 | pub(crate) fn text_edit(mut self, text_edit: AtomTextEdit) -> Builder<'a> { | ||
195 | self.text_edit = Some(text_edit); | ||
196 | self | ||
197 | } | ||
198 | pub(crate) fn additional_text_edits(mut self, additional_text_edits: TextEdit) -> Builder<'a> { | ||
199 | self.additional_text_edits = Some(additional_text_edits); | ||
200 | self | ||
201 | } | ||
134 | #[allow(unused)] | 202 | #[allow(unused)] |
135 | pub(crate) fn detail(self, detail: impl Into<String>) -> Builder { | 203 | pub(crate) fn detail(self, detail: impl Into<String>) -> Builder<'a> { |
136 | self.set_detail(Some(detail)) | 204 | self.set_detail(Some(detail)) |
137 | } | 205 | } |
138 | pub(crate) fn set_detail(mut self, detail: Option<impl Into<String>>) -> Builder { | 206 | pub(crate) fn set_detail(mut self, detail: Option<impl Into<String>>) -> Builder<'a> { |
139 | self.detail = detail.map(Into::into); | 207 | self.detail = detail.map(Into::into); |
140 | self | 208 | self |
141 | } | 209 | } |
@@ -143,7 +211,7 @@ impl Builder { | |||
143 | mut self, | 211 | mut self, |
144 | ctx: &CompletionContext, | 212 | ctx: &CompletionContext, |
145 | resolution: &hir::Resolution, | 213 | resolution: &hir::Resolution, |
146 | ) -> Builder { | 214 | ) -> Builder<'a> { |
147 | let resolved = resolution.def_id.map(|d| d.resolve(ctx.db)); | 215 | let resolved = resolution.def_id.map(|d| d.resolve(ctx.db)); |
148 | let kind = match resolved { | 216 | let kind = match resolved { |
149 | PerNs { | 217 | PerNs { |
@@ -188,21 +256,22 @@ impl Builder { | |||
188 | mut self, | 256 | mut self, |
189 | ctx: &CompletionContext, | 257 | ctx: &CompletionContext, |
190 | function: hir::Function, | 258 | function: hir::Function, |
191 | ) -> Builder { | 259 | ) -> Builder<'a> { |
192 | // If not an import, add parenthesis automatically. | 260 | // If not an import, add parenthesis automatically. |
193 | if ctx.use_item_syntax.is_none() && !ctx.is_call { | 261 | if ctx.use_item_syntax.is_none() && !ctx.is_call { |
194 | if function.signature(ctx.db).params().is_empty() { | 262 | if function.signature(ctx.db).params().is_empty() { |
195 | self.snippet = Some(format!("{}()$0", self.label)); | 263 | self.insert_text = Some(format!("{}()$0", self.label)); |
196 | } else { | 264 | } else { |
197 | self.snippet = Some(format!("{}($0)", self.label)); | 265 | self.insert_text = Some(format!("{}($0)", self.label)); |
198 | } | 266 | } |
267 | self.insert_text_format = InsertTextFormat::Snippet; | ||
199 | } | 268 | } |
200 | self.kind = Some(CompletionItemKind::Function); | 269 | self.kind = Some(CompletionItemKind::Function); |
201 | self | 270 | self |
202 | } | 271 | } |
203 | } | 272 | } |
204 | 273 | ||
205 | impl Into<CompletionItem> for Builder { | 274 | impl<'a> Into<CompletionItem> for Builder<'a> { |
206 | fn into(self) -> CompletionItem { | 275 | fn into(self) -> CompletionItem { |
207 | self.build() | 276 | self.build() |
208 | } | 277 | } |
@@ -225,60 +294,6 @@ impl Completions { | |||
225 | { | 294 | { |
226 | items.into_iter().for_each(|item| self.add(item.into())) | 295 | items.into_iter().for_each(|item| self.add(item.into())) |
227 | } | 296 | } |
228 | |||
229 | #[cfg(test)] | ||
230 | pub(crate) fn assert_match(&self, expected: &str, kind: CompletionKind) { | ||
231 | let expected = normalize(expected); | ||
232 | let actual = self.debug_render(kind); | ||
233 | test_utils::assert_eq_text!(expected.as_str(), actual.as_str(),); | ||
234 | |||
235 | /// Normalize the textual representation of `Completions`: | ||
236 | /// replace `;` with newlines, normalize whitespace | ||
237 | fn normalize(expected: &str) -> String { | ||
238 | use ra_syntax::{tokenize, TextUnit, TextRange, SyntaxKind::SEMI}; | ||
239 | let mut res = String::new(); | ||
240 | for line in expected.trim().lines() { | ||
241 | let line = line.trim(); | ||
242 | let mut start_offset: TextUnit = 0.into(); | ||
243 | // Yep, we use rust tokenize in completion tests :-) | ||
244 | for token in tokenize(line) { | ||
245 | let range = TextRange::offset_len(start_offset, token.len); | ||
246 | start_offset += token.len; | ||
247 | if token.kind == SEMI { | ||
248 | res.push('\n'); | ||
249 | } else { | ||
250 | res.push_str(&line[range]); | ||
251 | } | ||
252 | } | ||
253 | |||
254 | res.push('\n'); | ||
255 | } | ||
256 | res | ||
257 | } | ||
258 | } | ||
259 | |||
260 | #[cfg(test)] | ||
261 | fn debug_render(&self, kind: CompletionKind) -> String { | ||
262 | let mut res = String::new(); | ||
263 | for c in self.buf.iter() { | ||
264 | if c.completion_kind == kind { | ||
265 | if let Some(lookup) = &c.lookup { | ||
266 | res.push_str(lookup); | ||
267 | res.push_str(&format!(" {:?}", c.label)); | ||
268 | } else { | ||
269 | res.push_str(&c.label); | ||
270 | } | ||
271 | if let Some(detail) = &c.detail { | ||
272 | res.push_str(&format!(" {:?}", detail)); | ||
273 | } | ||
274 | if let Some(snippet) = &c.snippet { | ||
275 | res.push_str(&format!(" {:?}", snippet)); | ||
276 | } | ||
277 | res.push('\n'); | ||
278 | } | ||
279 | } | ||
280 | res | ||
281 | } | ||
282 | } | 297 | } |
283 | 298 | ||
284 | impl Into<Vec<CompletionItem>> for Completions { | 299 | impl Into<Vec<CompletionItem>> for Completions { |
@@ -286,3 +301,22 @@ impl Into<Vec<CompletionItem>> for Completions { | |||
286 | self.buf | 301 | self.buf |
287 | } | 302 | } |
288 | } | 303 | } |
304 | |||
305 | #[cfg(test)] | ||
306 | pub(crate) fn check_completion(test_name: &str, code: &str, kind: CompletionKind) { | ||
307 | use crate::mock_analysis::{single_file_with_position, analysis_and_position}; | ||
308 | use crate::completion::completions; | ||
309 | use insta::assert_debug_snapshot_matches; | ||
310 | let (analysis, position) = if code.contains("//-") { | ||
311 | analysis_and_position(code) | ||
312 | } else { | ||
313 | single_file_with_position(code) | ||
314 | }; | ||
315 | let completions = completions(&analysis.db, position).unwrap(); | ||
316 | let completion_items: Vec<CompletionItem> = completions.into(); | ||
317 | let kind_completions: Vec<CompletionItem> = completion_items | ||
318 | .into_iter() | ||
319 | .filter(|c| c.completion_kind == kind) | ||
320 | .collect(); | ||
321 | assert_debug_snapshot_matches!(test_name, kind_completions); | ||
322 | } | ||