diff options
author | Seivan Heidari <[email protected]> | 2019-11-28 07:19:14 +0000 |
---|---|---|
committer | Seivan Heidari <[email protected]> | 2019-11-28 07:19:14 +0000 |
commit | 18a0937585b836ec5ed054b9ae48e0156ab6d9ef (patch) | |
tree | 9de2c0267ddcc00df717f90034d0843d751a851b /crates/ra_ide/src/typing.rs | |
parent | a7394b44c870f585eacfeb3036a33471aff49ff8 (diff) | |
parent | 484acc8a61d599662ed63a4cbda091d38a982551 (diff) |
Merge branch 'master' of https://github.com/rust-analyzer/rust-analyzer into feature/themes
Diffstat (limited to 'crates/ra_ide/src/typing.rs')
-rw-r--r-- | crates/ra_ide/src/typing.rs | 490 |
1 files changed, 490 insertions, 0 deletions
diff --git a/crates/ra_ide/src/typing.rs b/crates/ra_ide/src/typing.rs new file mode 100644 index 000000000..21e5be9b3 --- /dev/null +++ b/crates/ra_ide/src/typing.rs | |||
@@ -0,0 +1,490 @@ | |||
1 | //! This module handles auto-magic editing actions applied together with users | ||
2 | //! edits. For example, if the user typed | ||
3 | //! | ||
4 | //! ```text | ||
5 | //! foo | ||
6 | //! .bar() | ||
7 | //! .baz() | ||
8 | //! | // <- cursor is here | ||
9 | //! ``` | ||
10 | //! | ||
11 | //! and types `.` next, we want to indent the dot. | ||
12 | //! | ||
13 | //! Language server executes such typing assists synchronously. That is, they | ||
14 | //! block user's typing and should be pretty fast for this reason! | ||
15 | |||
16 | use ra_db::{FilePosition, SourceDatabase}; | ||
17 | use ra_fmt::leading_indent; | ||
18 | use ra_syntax::{ | ||
19 | algo::find_node_at_offset, | ||
20 | ast::{self, AstToken}, | ||
21 | AstNode, SmolStr, SourceFile, | ||
22 | SyntaxKind::*, | ||
23 | SyntaxToken, TextRange, TextUnit, TokenAtOffset, | ||
24 | }; | ||
25 | use ra_text_edit::TextEdit; | ||
26 | |||
27 | use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit}; | ||
28 | |||
29 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | ||
30 | let parse = db.parse(position.file_id); | ||
31 | let file = parse.tree(); | ||
32 | let comment = file | ||
33 | .syntax() | ||
34 | .token_at_offset(position.offset) | ||
35 | .left_biased() | ||
36 | .and_then(ast::Comment::cast)?; | ||
37 | |||
38 | if comment.kind().shape.is_block() { | ||
39 | return None; | ||
40 | } | ||
41 | |||
42 | let prefix = comment.prefix(); | ||
43 | let comment_range = comment.syntax().text_range(); | ||
44 | if position.offset < comment_range.start() + TextUnit::of_str(prefix) + TextUnit::from(1) { | ||
45 | return None; | ||
46 | } | ||
47 | |||
48 | // Continuing non-doc line comments (like this one :) ) is annoying | ||
49 | if prefix == "//" && comment_range.end() == position.offset { | ||
50 | return None; | ||
51 | } | ||
52 | |||
53 | let indent = node_indent(&file, comment.syntax())?; | ||
54 | let inserted = format!("\n{}{} ", indent, prefix); | ||
55 | let cursor_position = position.offset + TextUnit::of_str(&inserted); | ||
56 | let edit = TextEdit::insert(position.offset, inserted); | ||
57 | |||
58 | Some( | ||
59 | SourceChange::source_file_edit( | ||
60 | "on enter", | ||
61 | SourceFileEdit { edit, file_id: position.file_id }, | ||
62 | ) | ||
63 | .with_cursor(FilePosition { offset: cursor_position, file_id: position.file_id }), | ||
64 | ) | ||
65 | } | ||
66 | |||
67 | fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { | ||
68 | let ws = match file.syntax().token_at_offset(token.text_range().start()) { | ||
69 | TokenAtOffset::Between(l, r) => { | ||
70 | assert!(r == *token); | ||
71 | l | ||
72 | } | ||
73 | TokenAtOffset::Single(n) => { | ||
74 | assert!(n == *token); | ||
75 | return Some("".into()); | ||
76 | } | ||
77 | TokenAtOffset::None => unreachable!(), | ||
78 | }; | ||
79 | if ws.kind() != WHITESPACE { | ||
80 | return None; | ||
81 | } | ||
82 | let text = ws.text(); | ||
83 | let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0); | ||
84 | Some(text[pos..].into()) | ||
85 | } | ||
86 | |||
87 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; | ||
88 | |||
89 | pub(crate) fn on_char_typed( | ||
90 | db: &RootDatabase, | ||
91 | position: FilePosition, | ||
92 | char_typed: char, | ||
93 | ) -> Option<SourceChange> { | ||
94 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
95 | let file = &db.parse(position.file_id).tree(); | ||
96 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | ||
97 | let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?; | ||
98 | Some(single_file_change.into_source_change(position.file_id)) | ||
99 | } | ||
100 | |||
101 | fn on_char_typed_inner( | ||
102 | file: &SourceFile, | ||
103 | offset: TextUnit, | ||
104 | char_typed: char, | ||
105 | ) -> Option<SingleFileChange> { | ||
106 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
107 | match char_typed { | ||
108 | '.' => on_dot_typed(file, offset), | ||
109 | '=' => on_eq_typed(file, offset), | ||
110 | '>' => on_arrow_typed(file, offset), | ||
111 | _ => unreachable!(), | ||
112 | } | ||
113 | } | ||
114 | |||
115 | /// Returns an edit which should be applied after `=` was typed. Primarily, | ||
116 | /// this works when adding `let =`. | ||
117 | // FIXME: use a snippet completion instead of this hack here. | ||
118 | fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
119 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | ||
120 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
121 | if let_stmt.has_semi() { | ||
122 | return None; | ||
123 | } | ||
124 | if let Some(expr) = let_stmt.initializer() { | ||
125 | let expr_range = expr.syntax().text_range(); | ||
126 | if expr_range.contains(offset) && offset != expr_range.start() { | ||
127 | return None; | ||
128 | } | ||
129 | if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { | ||
130 | return None; | ||
131 | } | ||
132 | } else { | ||
133 | return None; | ||
134 | } | ||
135 | let offset = let_stmt.syntax().text_range().end(); | ||
136 | Some(SingleFileChange { | ||
137 | label: "add semicolon".to_string(), | ||
138 | edit: TextEdit::insert(offset, ";".to_string()), | ||
139 | cursor_position: None, | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | ||
144 | fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
145 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); | ||
146 | let whitespace = | ||
147 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; | ||
148 | |||
149 | let current_indent = { | ||
150 | let text = whitespace.text(); | ||
151 | let newline = text.rfind('\n')?; | ||
152 | &text[newline + 1..] | ||
153 | }; | ||
154 | let current_indent_len = TextUnit::of_str(current_indent); | ||
155 | |||
156 | // Make sure dot is a part of call chain | ||
157 | let field_expr = ast::FieldExpr::cast(whitespace.syntax().parent())?; | ||
158 | let prev_indent = leading_indent(field_expr.syntax())?; | ||
159 | let target_indent = format!(" {}", prev_indent); | ||
160 | let target_indent_len = TextUnit::of_str(&target_indent); | ||
161 | if current_indent_len == target_indent_len { | ||
162 | return None; | ||
163 | } | ||
164 | |||
165 | Some(SingleFileChange { | ||
166 | label: "reindent dot".to_string(), | ||
167 | edit: TextEdit::replace( | ||
168 | TextRange::from_to(offset - current_indent_len, offset), | ||
169 | target_indent, | ||
170 | ), | ||
171 | cursor_position: Some( | ||
172 | offset + target_indent_len - current_indent_len + TextUnit::of_char('.'), | ||
173 | ), | ||
174 | }) | ||
175 | } | ||
176 | |||
177 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` | ||
178 | fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
179 | let file_text = file.syntax().text(); | ||
180 | assert_eq!(file_text.char_at(offset), Some('>')); | ||
181 | let after_arrow = offset + TextUnit::of_char('>'); | ||
182 | if file_text.char_at(after_arrow) != Some('{') { | ||
183 | return None; | ||
184 | } | ||
185 | if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() { | ||
186 | return None; | ||
187 | } | ||
188 | |||
189 | Some(SingleFileChange { | ||
190 | label: "add space after return type".to_string(), | ||
191 | edit: TextEdit::insert(after_arrow, " ".to_string()), | ||
192 | cursor_position: Some(after_arrow), | ||
193 | }) | ||
194 | } | ||
195 | |||
196 | #[cfg(test)] | ||
197 | mod tests { | ||
198 | use test_utils::{add_cursor, assert_eq_text, extract_offset}; | ||
199 | |||
200 | use crate::mock_analysis::single_file; | ||
201 | |||
202 | use super::*; | ||
203 | |||
204 | #[test] | ||
205 | fn test_on_enter() { | ||
206 | fn apply_on_enter(before: &str) -> Option<String> { | ||
207 | let (offset, before) = extract_offset(before); | ||
208 | let (analysis, file_id) = single_file(&before); | ||
209 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; | ||
210 | |||
211 | assert_eq!(result.source_file_edits.len(), 1); | ||
212 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
213 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); | ||
214 | Some(actual) | ||
215 | } | ||
216 | |||
217 | fn do_check(before: &str, after: &str) { | ||
218 | let actual = apply_on_enter(before).unwrap(); | ||
219 | assert_eq_text!(after, &actual); | ||
220 | } | ||
221 | |||
222 | fn do_check_noop(text: &str) { | ||
223 | assert!(apply_on_enter(text).is_none()) | ||
224 | } | ||
225 | |||
226 | do_check( | ||
227 | r" | ||
228 | /// Some docs<|> | ||
229 | fn foo() { | ||
230 | } | ||
231 | ", | ||
232 | r" | ||
233 | /// Some docs | ||
234 | /// <|> | ||
235 | fn foo() { | ||
236 | } | ||
237 | ", | ||
238 | ); | ||
239 | do_check( | ||
240 | r" | ||
241 | impl S { | ||
242 | /// Some<|> docs. | ||
243 | fn foo() {} | ||
244 | } | ||
245 | ", | ||
246 | r" | ||
247 | impl S { | ||
248 | /// Some | ||
249 | /// <|> docs. | ||
250 | fn foo() {} | ||
251 | } | ||
252 | ", | ||
253 | ); | ||
254 | do_check( | ||
255 | r" | ||
256 | fn main() { | ||
257 | // Fix<|> me | ||
258 | let x = 1 + 1; | ||
259 | } | ||
260 | ", | ||
261 | r" | ||
262 | fn main() { | ||
263 | // Fix | ||
264 | // <|> me | ||
265 | let x = 1 + 1; | ||
266 | } | ||
267 | ", | ||
268 | ); | ||
269 | do_check_noop( | ||
270 | r" | ||
271 | fn main() { | ||
272 | // Fix me<|> | ||
273 | let x = 1 + 1; | ||
274 | } | ||
275 | ", | ||
276 | ); | ||
277 | |||
278 | do_check_noop(r"<|>//! docz"); | ||
279 | } | ||
280 | |||
281 | fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> { | ||
282 | let (offset, before) = extract_offset(before); | ||
283 | let edit = TextEdit::insert(offset, char_typed.to_string()); | ||
284 | let before = edit.apply(&before); | ||
285 | let parse = SourceFile::parse(&before); | ||
286 | on_char_typed_inner(&parse.tree(), offset, char_typed) | ||
287 | .map(|it| (it.edit.apply(&before), it)) | ||
288 | } | ||
289 | |||
290 | fn type_char(char_typed: char, before: &str, after: &str) { | ||
291 | let (actual, file_change) = do_type_char(char_typed, before) | ||
292 | .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed)); | ||
293 | |||
294 | if after.contains("<|>") { | ||
295 | let (offset, after) = extract_offset(after); | ||
296 | assert_eq_text!(&after, &actual); | ||
297 | assert_eq!(file_change.cursor_position, Some(offset)) | ||
298 | } else { | ||
299 | assert_eq_text!(after, &actual); | ||
300 | } | ||
301 | } | ||
302 | |||
303 | fn type_char_noop(char_typed: char, before: &str) { | ||
304 | let file_change = do_type_char(char_typed, before); | ||
305 | assert!(file_change.is_none()) | ||
306 | } | ||
307 | |||
308 | #[test] | ||
309 | fn test_on_eq_typed() { | ||
310 | // do_check(r" | ||
311 | // fn foo() { | ||
312 | // let foo =<|> | ||
313 | // } | ||
314 | // ", r" | ||
315 | // fn foo() { | ||
316 | // let foo =; | ||
317 | // } | ||
318 | // "); | ||
319 | type_char( | ||
320 | '=', | ||
321 | r" | ||
322 | fn foo() { | ||
323 | let foo <|> 1 + 1 | ||
324 | } | ||
325 | ", | ||
326 | r" | ||
327 | fn foo() { | ||
328 | let foo = 1 + 1; | ||
329 | } | ||
330 | ", | ||
331 | ); | ||
332 | // do_check(r" | ||
333 | // fn foo() { | ||
334 | // let foo =<|> | ||
335 | // let bar = 1; | ||
336 | // } | ||
337 | // ", r" | ||
338 | // fn foo() { | ||
339 | // let foo =; | ||
340 | // let bar = 1; | ||
341 | // } | ||
342 | // "); | ||
343 | } | ||
344 | |||
345 | #[test] | ||
346 | fn indents_new_chain_call() { | ||
347 | type_char( | ||
348 | '.', | ||
349 | r" | ||
350 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
351 | self.child_impl(db, name) | ||
352 | <|> | ||
353 | } | ||
354 | ", | ||
355 | r" | ||
356 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
357 | self.child_impl(db, name) | ||
358 | . | ||
359 | } | ||
360 | ", | ||
361 | ); | ||
362 | type_char_noop( | ||
363 | '.', | ||
364 | r" | ||
365 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
366 | self.child_impl(db, name) | ||
367 | <|> | ||
368 | } | ||
369 | ", | ||
370 | ) | ||
371 | } | ||
372 | |||
373 | #[test] | ||
374 | fn indents_new_chain_call_with_semi() { | ||
375 | type_char( | ||
376 | '.', | ||
377 | r" | ||
378 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
379 | self.child_impl(db, name) | ||
380 | <|>; | ||
381 | } | ||
382 | ", | ||
383 | r" | ||
384 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
385 | self.child_impl(db, name) | ||
386 | .; | ||
387 | } | ||
388 | ", | ||
389 | ); | ||
390 | type_char_noop( | ||
391 | '.', | ||
392 | r" | ||
393 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
394 | self.child_impl(db, name) | ||
395 | <|>; | ||
396 | } | ||
397 | ", | ||
398 | ) | ||
399 | } | ||
400 | |||
401 | #[test] | ||
402 | fn indents_continued_chain_call() { | ||
403 | type_char( | ||
404 | '.', | ||
405 | r" | ||
406 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
407 | self.child_impl(db, name) | ||
408 | .first() | ||
409 | <|> | ||
410 | } | ||
411 | ", | ||
412 | r" | ||
413 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
414 | self.child_impl(db, name) | ||
415 | .first() | ||
416 | . | ||
417 | } | ||
418 | ", | ||
419 | ); | ||
420 | type_char_noop( | ||
421 | '.', | ||
422 | r" | ||
423 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
424 | self.child_impl(db, name) | ||
425 | .first() | ||
426 | <|> | ||
427 | } | ||
428 | ", | ||
429 | ); | ||
430 | } | ||
431 | |||
432 | #[test] | ||
433 | fn indents_middle_of_chain_call() { | ||
434 | type_char( | ||
435 | '.', | ||
436 | r" | ||
437 | fn source_impl() { | ||
438 | let var = enum_defvariant_list().unwrap() | ||
439 | <|> | ||
440 | .nth(92) | ||
441 | .unwrap(); | ||
442 | } | ||
443 | ", | ||
444 | r" | ||
445 | fn source_impl() { | ||
446 | let var = enum_defvariant_list().unwrap() | ||
447 | . | ||
448 | .nth(92) | ||
449 | .unwrap(); | ||
450 | } | ||
451 | ", | ||
452 | ); | ||
453 | type_char_noop( | ||
454 | '.', | ||
455 | r" | ||
456 | fn source_impl() { | ||
457 | let var = enum_defvariant_list().unwrap() | ||
458 | <|> | ||
459 | .nth(92) | ||
460 | .unwrap(); | ||
461 | } | ||
462 | ", | ||
463 | ); | ||
464 | } | ||
465 | |||
466 | #[test] | ||
467 | fn dont_indent_freestanding_dot() { | ||
468 | type_char_noop( | ||
469 | '.', | ||
470 | r" | ||
471 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
472 | <|> | ||
473 | } | ||
474 | ", | ||
475 | ); | ||
476 | type_char_noop( | ||
477 | '.', | ||
478 | r" | ||
479 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
480 | <|> | ||
481 | } | ||
482 | ", | ||
483 | ); | ||
484 | } | ||
485 | |||
486 | #[test] | ||
487 | fn adds_space_after_return_type() { | ||
488 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") | ||
489 | } | ||
490 | } | ||