diff options
Diffstat (limited to 'crates/ra_ide_api/src')
-rw-r--r-- | crates/ra_ide_api/src/lib.rs | 7 | ||||
-rw-r--r-- | crates/ra_ide_api/src/typing.rs | 408 |
2 files changed, 412 insertions, 3 deletions
diff --git a/crates/ra_ide_api/src/lib.rs b/crates/ra_ide_api/src/lib.rs index a838c30da..c2ef61ae2 100644 --- a/crates/ra_ide_api/src/lib.rs +++ b/crates/ra_ide_api/src/lib.rs | |||
@@ -37,6 +37,7 @@ mod line_index; | |||
37 | mod folding_ranges; | 37 | mod folding_ranges; |
38 | mod line_index_utils; | 38 | mod line_index_utils; |
39 | mod join_lines; | 39 | mod join_lines; |
40 | mod typing; | ||
40 | 41 | ||
41 | #[cfg(test)] | 42 | #[cfg(test)] |
42 | mod marks; | 43 | mod marks; |
@@ -295,7 +296,7 @@ impl Analysis { | |||
295 | /// up minor stuff like continuing the comment. | 296 | /// up minor stuff like continuing the comment. |
296 | pub fn on_enter(&self, position: FilePosition) -> Option<SourceChange> { | 297 | pub fn on_enter(&self, position: FilePosition) -> Option<SourceChange> { |
297 | let file = self.db.parse(position.file_id); | 298 | let file = self.db.parse(position.file_id); |
298 | let edit = ra_ide_api_light::on_enter(&file, position.offset)?; | 299 | let edit = typing::on_enter(&file, position.offset)?; |
299 | Some(SourceChange::from_local_edit(position.file_id, edit)) | 300 | Some(SourceChange::from_local_edit(position.file_id, edit)) |
300 | } | 301 | } |
301 | 302 | ||
@@ -304,14 +305,14 @@ impl Analysis { | |||
304 | // FIXME: use a snippet completion instead of this hack here. | 305 | // FIXME: use a snippet completion instead of this hack here. |
305 | pub fn on_eq_typed(&self, position: FilePosition) -> Option<SourceChange> { | 306 | pub fn on_eq_typed(&self, position: FilePosition) -> Option<SourceChange> { |
306 | let file = self.db.parse(position.file_id); | 307 | let file = self.db.parse(position.file_id); |
307 | let edit = ra_ide_api_light::on_eq_typed(&file, position.offset)?; | 308 | let edit = typing::on_eq_typed(&file, position.offset)?; |
308 | Some(SourceChange::from_local_edit(position.file_id, edit)) | 309 | Some(SourceChange::from_local_edit(position.file_id, edit)) |
309 | } | 310 | } |
310 | 311 | ||
311 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | 312 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. |
312 | pub fn on_dot_typed(&self, position: FilePosition) -> Option<SourceChange> { | 313 | pub fn on_dot_typed(&self, position: FilePosition) -> Option<SourceChange> { |
313 | let file = self.db.parse(position.file_id); | 314 | let file = self.db.parse(position.file_id); |
314 | let edit = ra_ide_api_light::on_dot_typed(&file, position.offset)?; | 315 | let edit = typing::on_dot_typed(&file, position.offset)?; |
315 | Some(SourceChange::from_local_edit(position.file_id, edit)) | 316 | Some(SourceChange::from_local_edit(position.file_id, edit)) |
316 | } | 317 | } |
317 | 318 | ||
diff --git a/crates/ra_ide_api/src/typing.rs b/crates/ra_ide_api/src/typing.rs new file mode 100644 index 000000000..b7e023d60 --- /dev/null +++ b/crates/ra_ide_api/src/typing.rs | |||
@@ -0,0 +1,408 @@ | |||
1 | use ra_syntax::{ | ||
2 | AstNode, SourceFile, SyntaxKind::*, | ||
3 | SyntaxNode, TextUnit, TextRange, | ||
4 | algo::{find_node_at_offset, find_leaf_at_offset, LeafAtOffset}, | ||
5 | ast::{self, AstToken}, | ||
6 | }; | ||
7 | use ra_fmt::leading_indent; | ||
8 | use crate::LocalEdit; | ||
9 | use ra_text_edit::TextEditBuilder; | ||
10 | |||
11 | pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { | ||
12 | let comment = | ||
13 | find_leaf_at_offset(file.syntax(), offset).left_biased().and_then(ast::Comment::cast)?; | ||
14 | |||
15 | if let ast::CommentFlavor::Multiline = comment.flavor() { | ||
16 | return None; | ||
17 | } | ||
18 | |||
19 | let prefix = comment.prefix(); | ||
20 | if offset < comment.syntax().range().start() + TextUnit::of_str(prefix) + TextUnit::from(1) { | ||
21 | return None; | ||
22 | } | ||
23 | |||
24 | let indent = node_indent(file, comment.syntax())?; | ||
25 | let inserted = format!("\n{}{} ", indent, prefix); | ||
26 | let cursor_position = offset + TextUnit::of_str(&inserted); | ||
27 | let mut edit = TextEditBuilder::default(); | ||
28 | edit.insert(offset, inserted); | ||
29 | Some(LocalEdit { | ||
30 | label: "on enter".to_string(), | ||
31 | edit: edit.finish(), | ||
32 | cursor_position: Some(cursor_position), | ||
33 | }) | ||
34 | } | ||
35 | |||
36 | fn node_indent<'a>(file: &'a SourceFile, node: &SyntaxNode) -> Option<&'a str> { | ||
37 | let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) { | ||
38 | LeafAtOffset::Between(l, r) => { | ||
39 | assert!(r == node); | ||
40 | l | ||
41 | } | ||
42 | LeafAtOffset::Single(n) => { | ||
43 | assert!(n == node); | ||
44 | return Some(""); | ||
45 | } | ||
46 | LeafAtOffset::None => unreachable!(), | ||
47 | }; | ||
48 | if ws.kind() != WHITESPACE { | ||
49 | return None; | ||
50 | } | ||
51 | let text = ws.leaf_text().unwrap(); | ||
52 | let pos = text.as_str().rfind('\n').map(|it| it + 1).unwrap_or(0); | ||
53 | Some(&text[pos..]) | ||
54 | } | ||
55 | |||
56 | pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<LocalEdit> { | ||
57 | assert_eq!(file.syntax().text().char_at(eq_offset), Some('=')); | ||
58 | let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?; | ||
59 | if let_stmt.has_semi() { | ||
60 | return None; | ||
61 | } | ||
62 | if let Some(expr) = let_stmt.initializer() { | ||
63 | let expr_range = expr.syntax().range(); | ||
64 | if expr_range.contains(eq_offset) && eq_offset != expr_range.start() { | ||
65 | return None; | ||
66 | } | ||
67 | if file.syntax().text().slice(eq_offset..expr_range.start()).contains('\n') { | ||
68 | return None; | ||
69 | } | ||
70 | } else { | ||
71 | return None; | ||
72 | } | ||
73 | let offset = let_stmt.syntax().range().end(); | ||
74 | let mut edit = TextEditBuilder::default(); | ||
75 | edit.insert(offset, ";".to_string()); | ||
76 | Some(LocalEdit { | ||
77 | label: "add semicolon".to_string(), | ||
78 | edit: edit.finish(), | ||
79 | cursor_position: None, | ||
80 | }) | ||
81 | } | ||
82 | |||
83 | pub fn on_dot_typed(file: &SourceFile, dot_offset: TextUnit) -> Option<LocalEdit> { | ||
84 | assert_eq!(file.syntax().text().char_at(dot_offset), Some('.')); | ||
85 | |||
86 | let whitespace = find_leaf_at_offset(file.syntax(), dot_offset) | ||
87 | .left_biased() | ||
88 | .and_then(ast::Whitespace::cast)?; | ||
89 | |||
90 | let current_indent = { | ||
91 | let text = whitespace.text(); | ||
92 | let newline = text.rfind('\n')?; | ||
93 | &text[newline + 1..] | ||
94 | }; | ||
95 | let current_indent_len = TextUnit::of_str(current_indent); | ||
96 | |||
97 | // Make sure dot is a part of call chain | ||
98 | let field_expr = whitespace.syntax().parent().and_then(ast::FieldExpr::cast)?; | ||
99 | let prev_indent = leading_indent(field_expr.syntax())?; | ||
100 | let target_indent = format!(" {}", prev_indent); | ||
101 | let target_indent_len = TextUnit::of_str(&target_indent); | ||
102 | if current_indent_len == target_indent_len { | ||
103 | return None; | ||
104 | } | ||
105 | let mut edit = TextEditBuilder::default(); | ||
106 | edit.replace( | ||
107 | TextRange::from_to(dot_offset - current_indent_len, dot_offset), | ||
108 | target_indent.into(), | ||
109 | ); | ||
110 | let res = LocalEdit { | ||
111 | label: "reindent dot".to_string(), | ||
112 | edit: edit.finish(), | ||
113 | cursor_position: Some( | ||
114 | dot_offset + target_indent_len - current_indent_len + TextUnit::of_char('.'), | ||
115 | ), | ||
116 | }; | ||
117 | Some(res) | ||
118 | } | ||
119 | |||
120 | #[cfg(test)] | ||
121 | mod tests { | ||
122 | use test_utils::{add_cursor, assert_eq_text, extract_offset}; | ||
123 | |||
124 | use super::*; | ||
125 | |||
126 | #[test] | ||
127 | fn test_on_eq_typed() { | ||
128 | fn type_eq(before: &str, after: &str) { | ||
129 | let (offset, before) = extract_offset(before); | ||
130 | let mut edit = TextEditBuilder::default(); | ||
131 | edit.insert(offset, "=".to_string()); | ||
132 | let before = edit.finish().apply(&before); | ||
133 | let file = SourceFile::parse(&before); | ||
134 | if let Some(result) = on_eq_typed(&file, offset) { | ||
135 | let actual = result.edit.apply(&before); | ||
136 | assert_eq_text!(after, &actual); | ||
137 | } else { | ||
138 | assert_eq_text!(&before, after) | ||
139 | }; | ||
140 | } | ||
141 | |||
142 | // do_check(r" | ||
143 | // fn foo() { | ||
144 | // let foo =<|> | ||
145 | // } | ||
146 | // ", r" | ||
147 | // fn foo() { | ||
148 | // let foo =; | ||
149 | // } | ||
150 | // "); | ||
151 | type_eq( | ||
152 | r" | ||
153 | fn foo() { | ||
154 | let foo <|> 1 + 1 | ||
155 | } | ||
156 | ", | ||
157 | r" | ||
158 | fn foo() { | ||
159 | let foo = 1 + 1; | ||
160 | } | ||
161 | ", | ||
162 | ); | ||
163 | // do_check(r" | ||
164 | // fn foo() { | ||
165 | // let foo =<|> | ||
166 | // let bar = 1; | ||
167 | // } | ||
168 | // ", r" | ||
169 | // fn foo() { | ||
170 | // let foo =; | ||
171 | // let bar = 1; | ||
172 | // } | ||
173 | // "); | ||
174 | } | ||
175 | |||
176 | fn type_dot(before: &str, after: &str) { | ||
177 | let (offset, before) = extract_offset(before); | ||
178 | let mut edit = TextEditBuilder::default(); | ||
179 | edit.insert(offset, ".".to_string()); | ||
180 | let before = edit.finish().apply(&before); | ||
181 | let file = SourceFile::parse(&before); | ||
182 | if let Some(result) = on_dot_typed(&file, offset) { | ||
183 | let actual = result.edit.apply(&before); | ||
184 | assert_eq_text!(after, &actual); | ||
185 | } else { | ||
186 | assert_eq_text!(&before, after) | ||
187 | }; | ||
188 | } | ||
189 | |||
190 | #[test] | ||
191 | fn indents_new_chain_call() { | ||
192 | type_dot( | ||
193 | r" | ||
194 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
195 | self.child_impl(db, name) | ||
196 | <|> | ||
197 | } | ||
198 | ", | ||
199 | r" | ||
200 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
201 | self.child_impl(db, name) | ||
202 | . | ||
203 | } | ||
204 | ", | ||
205 | ); | ||
206 | type_dot( | ||
207 | r" | ||
208 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
209 | self.child_impl(db, name) | ||
210 | <|> | ||
211 | } | ||
212 | ", | ||
213 | r" | ||
214 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
215 | self.child_impl(db, name) | ||
216 | . | ||
217 | } | ||
218 | ", | ||
219 | ) | ||
220 | } | ||
221 | |||
222 | #[test] | ||
223 | fn indents_new_chain_call_with_semi() { | ||
224 | type_dot( | ||
225 | r" | ||
226 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
227 | self.child_impl(db, name) | ||
228 | <|>; | ||
229 | } | ||
230 | ", | ||
231 | r" | ||
232 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
233 | self.child_impl(db, name) | ||
234 | .; | ||
235 | } | ||
236 | ", | ||
237 | ); | ||
238 | type_dot( | ||
239 | r" | ||
240 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
241 | self.child_impl(db, name) | ||
242 | <|>; | ||
243 | } | ||
244 | ", | ||
245 | r" | ||
246 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
247 | self.child_impl(db, name) | ||
248 | .; | ||
249 | } | ||
250 | ", | ||
251 | ) | ||
252 | } | ||
253 | |||
254 | #[test] | ||
255 | fn indents_continued_chain_call() { | ||
256 | type_dot( | ||
257 | r" | ||
258 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
259 | self.child_impl(db, name) | ||
260 | .first() | ||
261 | <|> | ||
262 | } | ||
263 | ", | ||
264 | r" | ||
265 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
266 | self.child_impl(db, name) | ||
267 | .first() | ||
268 | . | ||
269 | } | ||
270 | ", | ||
271 | ); | ||
272 | type_dot( | ||
273 | r" | ||
274 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
275 | self.child_impl(db, name) | ||
276 | .first() | ||
277 | <|> | ||
278 | } | ||
279 | ", | ||
280 | r" | ||
281 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
282 | self.child_impl(db, name) | ||
283 | .first() | ||
284 | . | ||
285 | } | ||
286 | ", | ||
287 | ); | ||
288 | } | ||
289 | |||
290 | #[test] | ||
291 | fn indents_middle_of_chain_call() { | ||
292 | type_dot( | ||
293 | r" | ||
294 | fn source_impl() { | ||
295 | let var = enum_defvariant_list().unwrap() | ||
296 | <|> | ||
297 | .nth(92) | ||
298 | .unwrap(); | ||
299 | } | ||
300 | ", | ||
301 | r" | ||
302 | fn source_impl() { | ||
303 | let var = enum_defvariant_list().unwrap() | ||
304 | . | ||
305 | .nth(92) | ||
306 | .unwrap(); | ||
307 | } | ||
308 | ", | ||
309 | ); | ||
310 | type_dot( | ||
311 | r" | ||
312 | fn source_impl() { | ||
313 | let var = enum_defvariant_list().unwrap() | ||
314 | <|> | ||
315 | .nth(92) | ||
316 | .unwrap(); | ||
317 | } | ||
318 | ", | ||
319 | r" | ||
320 | fn source_impl() { | ||
321 | let var = enum_defvariant_list().unwrap() | ||
322 | . | ||
323 | .nth(92) | ||
324 | .unwrap(); | ||
325 | } | ||
326 | ", | ||
327 | ); | ||
328 | } | ||
329 | |||
330 | #[test] | ||
331 | fn dont_indent_freestanding_dot() { | ||
332 | type_dot( | ||
333 | r" | ||
334 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
335 | <|> | ||
336 | } | ||
337 | ", | ||
338 | r" | ||
339 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
340 | . | ||
341 | } | ||
342 | ", | ||
343 | ); | ||
344 | type_dot( | ||
345 | r" | ||
346 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
347 | <|> | ||
348 | } | ||
349 | ", | ||
350 | r" | ||
351 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
352 | . | ||
353 | } | ||
354 | ", | ||
355 | ); | ||
356 | } | ||
357 | |||
358 | #[test] | ||
359 | fn test_on_enter() { | ||
360 | fn apply_on_enter(before: &str) -> Option<String> { | ||
361 | let (offset, before) = extract_offset(before); | ||
362 | let file = SourceFile::parse(&before); | ||
363 | let result = on_enter(&file, offset)?; | ||
364 | let actual = result.edit.apply(&before); | ||
365 | let actual = add_cursor(&actual, result.cursor_position.unwrap()); | ||
366 | Some(actual) | ||
367 | } | ||
368 | |||
369 | fn do_check(before: &str, after: &str) { | ||
370 | let actual = apply_on_enter(before).unwrap(); | ||
371 | assert_eq_text!(after, &actual); | ||
372 | } | ||
373 | |||
374 | fn do_check_noop(text: &str) { | ||
375 | assert!(apply_on_enter(text).is_none()) | ||
376 | } | ||
377 | |||
378 | do_check( | ||
379 | r" | ||
380 | /// Some docs<|> | ||
381 | fn foo() { | ||
382 | } | ||
383 | ", | ||
384 | r" | ||
385 | /// Some docs | ||
386 | /// <|> | ||
387 | fn foo() { | ||
388 | } | ||
389 | ", | ||
390 | ); | ||
391 | do_check( | ||
392 | r" | ||
393 | impl S { | ||
394 | /// Some<|> docs. | ||
395 | fn foo() {} | ||
396 | } | ||
397 | ", | ||
398 | r" | ||
399 | impl S { | ||
400 | /// Some | ||
401 | /// <|> docs. | ||
402 | fn foo() {} | ||
403 | } | ||
404 | ", | ||
405 | ); | ||
406 | do_check_noop(r"<|>//! docz"); | ||
407 | } | ||
408 | } | ||