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 | 165 |
1 files changed, 85 insertions, 80 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..f46d9e581 100644 --- a/crates/ra_ide_api/src/completion/completion_item.rs +++ b/crates/ra_ide_api/src/completion/completion_item.rs | |||
@@ -1,6 +1,8 @@ | |||
1 | use hir::PerNs; | 1 | use hir::PerNs; |
2 | 2 | ||
3 | use crate::completion::CompletionContext; | 3 | use crate::completion::completion_context::CompletionContext; |
4 | use ra_syntax::TextRange; | ||
5 | use ra_text_edit::TextEdit; | ||
4 | 6 | ||
5 | /// `CompletionItem` describes a single completion variant in the editor pop-up. | 7 | /// `CompletionItem` describes a single completion variant in the editor pop-up. |
6 | /// It is basically a POD with various properties. To construct a | 8 | /// It is basically a POD with various properties. To construct a |
@@ -11,15 +13,13 @@ pub struct CompletionItem { | |||
11 | /// completion. | 13 | /// completion. |
12 | completion_kind: CompletionKind, | 14 | completion_kind: CompletionKind, |
13 | label: String, | 15 | label: String, |
16 | kind: Option<CompletionItemKind>, | ||
14 | detail: Option<String>, | 17 | detail: Option<String>, |
15 | lookup: Option<String>, | 18 | lookup: Option<String>, |
16 | snippet: Option<String>, | 19 | insert_text: Option<String>, |
17 | kind: Option<CompletionItemKind>, | 20 | insert_text_format: InsertTextFormat, |
18 | } | 21 | source_range: TextRange, |
19 | 22 | text_edit: Option<TextEdit>, | |
20 | pub enum InsertText { | ||
21 | PlainText { text: String }, | ||
22 | Snippet { text: String }, | ||
23 | } | 23 | } |
24 | 24 | ||
25 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
@@ -40,7 +40,7 @@ pub enum CompletionItemKind { | |||
40 | Method, | 40 | Method, |
41 | } | 41 | } |
42 | 42 | ||
43 | #[derive(Debug, PartialEq, Eq)] | 43 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] |
44 | pub(crate) enum CompletionKind { | 44 | pub(crate) enum CompletionKind { |
45 | /// Parser-based keyword completion. | 45 | /// Parser-based keyword completion. |
46 | Keyword, | 46 | Keyword, |
@@ -51,16 +51,29 @@ pub(crate) enum CompletionKind { | |||
51 | Snippet, | 51 | Snippet, |
52 | } | 52 | } |
53 | 53 | ||
54 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
55 | pub enum InsertTextFormat { | ||
56 | PlainText, | ||
57 | Snippet, | ||
58 | } | ||
59 | |||
54 | impl CompletionItem { | 60 | impl CompletionItem { |
55 | pub(crate) fn new(completion_kind: CompletionKind, label: impl Into<String>) -> Builder { | 61 | pub(crate) fn new( |
62 | completion_kind: CompletionKind, | ||
63 | replace_range: TextRange, | ||
64 | label: impl Into<String>, | ||
65 | ) -> Builder { | ||
56 | let label = label.into(); | 66 | let label = label.into(); |
57 | Builder { | 67 | Builder { |
68 | source_range: replace_range, | ||
58 | completion_kind, | 69 | completion_kind, |
59 | label, | 70 | label, |
71 | insert_text: None, | ||
72 | insert_text_format: InsertTextFormat::PlainText, | ||
60 | detail: None, | 73 | detail: None, |
61 | lookup: None, | 74 | lookup: None, |
62 | snippet: None, | ||
63 | kind: None, | 75 | kind: None, |
76 | text_edit: None, | ||
64 | } | 77 | } |
65 | } | 78 | } |
66 | /// What user sees in pop-up in the UI. | 79 | /// What user sees in pop-up in the UI. |
@@ -78,30 +91,39 @@ impl CompletionItem { | |||
78 | .map(|it| it.as_str()) | 91 | .map(|it| it.as_str()) |
79 | .unwrap_or(self.label()) | 92 | .unwrap_or(self.label()) |
80 | } | 93 | } |
81 | /// What is inserted. | 94 | |
82 | pub fn insert_text(&self) -> InsertText { | 95 | pub fn insert_text_format(&self) -> InsertTextFormat { |
83 | match &self.snippet { | 96 | self.insert_text_format.clone() |
84 | None => InsertText::PlainText { | 97 | } |
85 | text: self.label.clone(), | 98 | pub fn insert_text(&self) -> String { |
86 | }, | 99 | match &self.insert_text { |
87 | Some(it) => InsertText::Snippet { text: it.clone() }, | 100 | Some(t) => t.clone(), |
101 | None => self.label.clone(), | ||
88 | } | 102 | } |
89 | } | 103 | } |
90 | |||
91 | pub fn kind(&self) -> Option<CompletionItemKind> { | 104 | pub fn kind(&self) -> Option<CompletionItemKind> { |
92 | self.kind | 105 | self.kind |
93 | } | 106 | } |
107 | pub fn take_text_edit(&mut self) -> Option<TextEdit> { | ||
108 | self.text_edit.take() | ||
109 | } | ||
110 | pub fn source_range(&self) -> TextRange { | ||
111 | self.source_range | ||
112 | } | ||
94 | } | 113 | } |
95 | 114 | ||
96 | /// A helper to make `CompletionItem`s. | 115 | /// A helper to make `CompletionItem`s. |
97 | #[must_use] | 116 | #[must_use] |
98 | pub(crate) struct Builder { | 117 | pub(crate) struct Builder { |
118 | source_range: TextRange, | ||
99 | completion_kind: CompletionKind, | 119 | completion_kind: CompletionKind, |
100 | label: String, | 120 | label: String, |
121 | insert_text: Option<String>, | ||
122 | insert_text_format: InsertTextFormat, | ||
101 | detail: Option<String>, | 123 | detail: Option<String>, |
102 | lookup: Option<String>, | 124 | lookup: Option<String>, |
103 | snippet: Option<String>, | ||
104 | kind: Option<CompletionItemKind>, | 125 | kind: Option<CompletionItemKind>, |
126 | text_edit: Option<TextEdit>, | ||
105 | } | 127 | } |
106 | 128 | ||
107 | impl Builder { | 129 | impl Builder { |
@@ -111,27 +133,44 @@ impl Builder { | |||
111 | 133 | ||
112 | pub(crate) fn build(self) -> CompletionItem { | 134 | pub(crate) fn build(self) -> CompletionItem { |
113 | CompletionItem { | 135 | CompletionItem { |
136 | source_range: self.source_range, | ||
114 | label: self.label, | 137 | label: self.label, |
115 | detail: self.detail, | 138 | detail: self.detail, |
139 | insert_text_format: self.insert_text_format, | ||
116 | lookup: self.lookup, | 140 | lookup: self.lookup, |
117 | snippet: self.snippet, | ||
118 | kind: self.kind, | 141 | kind: self.kind, |
119 | completion_kind: self.completion_kind, | 142 | completion_kind: self.completion_kind, |
143 | text_edit: self.text_edit, | ||
144 | insert_text: self.insert_text, | ||
120 | } | 145 | } |
121 | } | 146 | } |
122 | pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder { | 147 | pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder { |
123 | self.lookup = Some(lookup.into()); | 148 | self.lookup = Some(lookup.into()); |
124 | self | 149 | self |
125 | } | 150 | } |
126 | pub(crate) fn snippet(mut self, snippet: impl Into<String>) -> Builder { | 151 | pub(crate) fn insert_text(mut self, insert_text: impl Into<String>) -> Builder { |
127 | self.snippet = Some(snippet.into()); | 152 | self.insert_text = Some(insert_text.into()); |
153 | self | ||
154 | } | ||
155 | #[allow(unused)] | ||
156 | pub(crate) fn insert_text_format(mut self, insert_text_format: InsertTextFormat) -> Builder { | ||
157 | self.insert_text_format = insert_text_format; | ||
128 | self | 158 | self |
129 | } | 159 | } |
160 | pub(crate) fn snippet(mut self, snippet: impl Into<String>) -> Builder { | ||
161 | self.insert_text_format = InsertTextFormat::Snippet; | ||
162 | self.insert_text(snippet) | ||
163 | } | ||
130 | pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { | 164 | pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { |
131 | self.kind = Some(kind); | 165 | self.kind = Some(kind); |
132 | self | 166 | self |
133 | } | 167 | } |
134 | #[allow(unused)] | 168 | #[allow(unused)] |
169 | pub(crate) fn text_edit(mut self, edit: TextEdit) -> Builder { | ||
170 | self.text_edit = Some(edit); | ||
171 | self | ||
172 | } | ||
173 | #[allow(unused)] | ||
135 | pub(crate) fn detail(self, detail: impl Into<String>) -> Builder { | 174 | pub(crate) fn detail(self, detail: impl Into<String>) -> Builder { |
136 | self.set_detail(Some(detail)) | 175 | self.set_detail(Some(detail)) |
137 | } | 176 | } |
@@ -192,17 +231,18 @@ impl Builder { | |||
192 | // If not an import, add parenthesis automatically. | 231 | // If not an import, add parenthesis automatically. |
193 | if ctx.use_item_syntax.is_none() && !ctx.is_call { | 232 | if ctx.use_item_syntax.is_none() && !ctx.is_call { |
194 | if function.signature(ctx.db).params().is_empty() { | 233 | if function.signature(ctx.db).params().is_empty() { |
195 | self.snippet = Some(format!("{}()$0", self.label)); | 234 | self.insert_text = Some(format!("{}()$0", self.label)); |
196 | } else { | 235 | } else { |
197 | self.snippet = Some(format!("{}($0)", self.label)); | 236 | self.insert_text = Some(format!("{}($0)", self.label)); |
198 | } | 237 | } |
238 | self.insert_text_format = InsertTextFormat::Snippet; | ||
199 | } | 239 | } |
200 | self.kind = Some(CompletionItemKind::Function); | 240 | self.kind = Some(CompletionItemKind::Function); |
201 | self | 241 | self |
202 | } | 242 | } |
203 | } | 243 | } |
204 | 244 | ||
205 | impl Into<CompletionItem> for Builder { | 245 | impl<'a> Into<CompletionItem> for Builder { |
206 | fn into(self) -> CompletionItem { | 246 | fn into(self) -> CompletionItem { |
207 | self.build() | 247 | self.build() |
208 | } | 248 | } |
@@ -225,60 +265,6 @@ impl Completions { | |||
225 | { | 265 | { |
226 | items.into_iter().for_each(|item| self.add(item.into())) | 266 | items.into_iter().for_each(|item| self.add(item.into())) |
227 | } | 267 | } |
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 | } | 268 | } |
283 | 269 | ||
284 | impl Into<Vec<CompletionItem>> for Completions { | 270 | impl Into<Vec<CompletionItem>> for Completions { |
@@ -286,3 +272,22 @@ impl Into<Vec<CompletionItem>> for Completions { | |||
286 | self.buf | 272 | self.buf |
287 | } | 273 | } |
288 | } | 274 | } |
275 | |||
276 | #[cfg(test)] | ||
277 | pub(crate) fn check_completion(test_name: &str, code: &str, kind: CompletionKind) { | ||
278 | use crate::mock_analysis::{single_file_with_position, analysis_and_position}; | ||
279 | use crate::completion::completions; | ||
280 | use insta::assert_debug_snapshot_matches; | ||
281 | let (analysis, position) = if code.contains("//-") { | ||
282 | analysis_and_position(code) | ||
283 | } else { | ||
284 | single_file_with_position(code) | ||
285 | }; | ||
286 | let completions = completions(&analysis.db, position).unwrap(); | ||
287 | let completion_items: Vec<CompletionItem> = completions.into(); | ||
288 | let kind_completions: Vec<CompletionItem> = completion_items | ||
289 | .into_iter() | ||
290 | .filter(|c| c.completion_kind == kind) | ||
291 | .collect(); | ||
292 | assert_debug_snapshot_matches!(test_name, kind_completions); | ||
293 | } | ||