aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api/src/completion/completion_item.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api/src/completion/completion_item.rs')
-rw-r--r--crates/ra_ide_api/src/completion/completion_item.rs210
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 @@
1use hir::PerNs; 1use hir::PerNs;
2use ra_text_edit::{
3 AtomTextEdit,
4 TextEdit,
5};
2 6
3use crate::completion::CompletionContext; 7use 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
20pub 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)]
44pub(crate) enum CompletionKind { 62pub(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)]
73pub enum InsertTextFormat {
74 PlainText,
75 Snippet,
76}
77
54impl CompletionItem { 78impl 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]
98pub(crate) struct Builder { 131pub(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
107impl Builder { 144impl<'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
205impl Into<CompletionItem> for Builder { 274impl<'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
284impl Into<Vec<CompletionItem>> for Completions { 299impl 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)]
306pub(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}