]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing/on_enter.rs
Merge #8791
[rust.git] / crates / ide / src / typing / on_enter.rs
1 //! Handles the `Enter` key press. At the momently, this only continues
2 //! comments, but should handle indent some time in the future as well.
3
4 use ide_db::base_db::{FilePosition, SourceDatabase};
5 use ide_db::RootDatabase;
6 use syntax::{
7     algo::find_node_at_offset,
8     ast::{self, edit::IndentLevel, AstToken},
9     AstNode, SmolStr, SourceFile,
10     SyntaxKind::*,
11     SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset,
12 };
13
14 use text_edit::TextEdit;
15
16 // Feature: On Enter
17 //
18 // rust-analyzer can override kbd:[Enter] key to make it smarter:
19 //
20 // - kbd:[Enter] inside triple-slash comments automatically inserts `///`
21 // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//`
22 // - kbd:[Enter] inside `//!` doc comments automatically inserts `//!`
23 // - kbd:[Enter] after `{` indents contents and closing `}` of single-line block
24 //
25 // This action needs to be assigned to shortcut explicitly.
26 //
27 // VS Code::
28 //
29 // Add the following to `keybindings.json`:
30 // [source,json]
31 // ----
32 // {
33 //   "key": "Enter",
34 //   "command": "rust-analyzer.onEnter",
35 //   "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust"
36 // }
37 // ----
38 //
39 // image::https://user-images.githubusercontent.com/48062697/113065578-04c21800-91b1-11eb-82b8-22b8c481e645.gif[]
40 pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
41     let parse = db.parse(position.file_id);
42     let file = parse.tree();
43     let token = file.syntax().token_at_offset(position.offset).left_biased()?;
44
45     if let Some(comment) = ast::Comment::cast(token.clone()) {
46         return on_enter_in_comment(&comment, &file, position.offset);
47     }
48
49     if token.kind() == L_CURLY {
50         // Typing enter after the `{` of a block expression, where the `}` is on the same line
51         if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{'))
52             .and_then(|block| on_enter_in_block(block, position))
53         {
54             cov_mark::hit!(indent_block_contents);
55             return Some(edit);
56         }
57
58         // Typing enter after the `{` of a use tree list.
59         if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{'))
60             .and_then(|list| on_enter_in_use_tree_list(list, position))
61         {
62             cov_mark::hit!(indent_block_contents);
63             return Some(edit);
64         }
65     }
66
67     None
68 }
69
70 fn on_enter_in_comment(
71     comment: &ast::Comment,
72     file: &ast::SourceFile,
73     offset: TextSize,
74 ) -> Option<TextEdit> {
75     if comment.kind().shape.is_block() {
76         return None;
77     }
78
79     let prefix = comment.prefix();
80     let comment_range = comment.syntax().text_range();
81     if offset < comment_range.start() + TextSize::of(prefix) {
82         return None;
83     }
84
85     let mut remove_trailing_whitespace = false;
86     // Continuing single-line non-doc comments (like this one :) ) is annoying
87     if prefix == "//" && comment_range.end() == offset {
88         if comment.text().ends_with(' ') {
89             cov_mark::hit!(continues_end_of_line_comment_with_space);
90             remove_trailing_whitespace = true;
91         } else if !followed_by_comment(&comment) {
92             return None;
93         }
94     }
95
96     let indent = node_indent(&file, comment.syntax())?;
97     let inserted = format!("\n{}{} $0", indent, prefix);
98     let delete = if remove_trailing_whitespace {
99         let trimmed_len = comment.text().trim_end().len() as u32;
100         let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len;
101         TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset)
102     } else {
103         TextRange::empty(offset)
104     };
105     let edit = TextEdit::replace(delete, inserted);
106     Some(edit)
107 }
108
109 fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<TextEdit> {
110     let contents = block_contents(&block)?;
111
112     if block.syntax().text().contains_char('\n') {
113         return None;
114     }
115
116     let indent = IndentLevel::from_node(block.syntax());
117     let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1));
118     edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{}", indent))).ok()?;
119     Some(edit)
120 }
121
122 fn on_enter_in_use_tree_list(list: ast::UseTreeList, position: FilePosition) -> Option<TextEdit> {
123     if list.syntax().text().contains_char('\n') {
124         return None;
125     }
126
127     let indent = IndentLevel::from_node(list.syntax());
128     let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1));
129     edit.union(TextEdit::insert(
130         list.r_curly_token()?.text_range().start(),
131         format!("\n{}", indent),
132     ))
133     .ok()?;
134     Some(edit)
135 }
136
137 fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> {
138     let mut node = block.tail_expr().map(|e| e.syntax().clone());
139
140     for stmt in block.statements() {
141         if node.is_some() {
142             // More than 1 node in the block
143             return None;
144         }
145
146         node = Some(stmt.syntax().clone());
147     }
148
149     node
150 }
151
152 fn followed_by_comment(comment: &ast::Comment) -> bool {
153     let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) {
154         Some(it) => it,
155         None => return false,
156     };
157     if ws.spans_multiple_lines() {
158         return false;
159     }
160     ws.syntax().next_token().and_then(ast::Comment::cast).is_some()
161 }
162
163 fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
164     let ws = match file.syntax().token_at_offset(token.text_range().start()) {
165         TokenAtOffset::Between(l, r) => {
166             assert!(r == *token);
167             l
168         }
169         TokenAtOffset::Single(n) => {
170             assert!(n == *token);
171             return Some("".into());
172         }
173         TokenAtOffset::None => unreachable!(),
174     };
175     if ws.kind() != WHITESPACE {
176         return None;
177     }
178     let text = ws.text();
179     let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0);
180     Some(text[pos..].into())
181 }
182
183 #[cfg(test)]
184 mod tests {
185     use stdx::trim_indent;
186     use test_utils::assert_eq_text;
187
188     use crate::fixture;
189
190     fn apply_on_enter(before: &str) -> Option<String> {
191         let (analysis, position) = fixture::position(&before);
192         let result = analysis.on_enter(position).unwrap()?;
193
194         let mut actual = analysis.file_text(position.file_id).unwrap().to_string();
195         result.apply(&mut actual);
196         Some(actual)
197     }
198
199     fn do_check(ra_fixture_before: &str, ra_fixture_after: &str) {
200         let ra_fixture_after = &trim_indent(ra_fixture_after);
201         let actual = apply_on_enter(ra_fixture_before).unwrap();
202         assert_eq_text!(ra_fixture_after, &actual);
203     }
204
205     fn do_check_noop(ra_fixture_text: &str) {
206         assert!(apply_on_enter(ra_fixture_text).is_none())
207     }
208
209     #[test]
210     fn continues_doc_comment() {
211         do_check(
212             r"
213 /// Some docs$0
214 fn foo() {
215 }
216 ",
217             r"
218 /// Some docs
219 /// $0
220 fn foo() {
221 }
222 ",
223         );
224
225         do_check(
226             r"
227 impl S {
228     /// Some$0 docs.
229     fn foo() {}
230 }
231 ",
232             r"
233 impl S {
234     /// Some
235     /// $0 docs.
236     fn foo() {}
237 }
238 ",
239         );
240
241         do_check(
242             r"
243 ///$0 Some docs
244 fn foo() {
245 }
246 ",
247             r"
248 ///
249 /// $0 Some docs
250 fn foo() {
251 }
252 ",
253         );
254     }
255
256     #[test]
257     fn does_not_continue_before_doc_comment() {
258         do_check_noop(r"$0//! docz");
259     }
260
261     #[test]
262     fn continues_another_doc_comment() {
263         do_check(
264             r#"
265 fn main() {
266     //! Documentation for$0 on enter
267     let x = 1 + 1;
268 }
269 "#,
270             r#"
271 fn main() {
272     //! Documentation for
273     //! $0 on enter
274     let x = 1 + 1;
275 }
276 "#,
277         );
278     }
279
280     #[test]
281     fn continues_code_comment_in_the_middle_of_line() {
282         do_check(
283             r"
284 fn main() {
285     // Fix$0 me
286     let x = 1 + 1;
287 }
288 ",
289             r"
290 fn main() {
291     // Fix
292     // $0 me
293     let x = 1 + 1;
294 }
295 ",
296         );
297     }
298
299     #[test]
300     fn continues_code_comment_in_the_middle_several_lines() {
301         do_check(
302             r"
303 fn main() {
304     // Fix$0
305     // me
306     let x = 1 + 1;
307 }
308 ",
309             r"
310 fn main() {
311     // Fix
312     // $0
313     // me
314     let x = 1 + 1;
315 }
316 ",
317         );
318     }
319
320     #[test]
321     fn does_not_continue_end_of_line_comment() {
322         do_check_noop(
323             r"
324 fn main() {
325     // Fix me$0
326     let x = 1 + 1;
327 }
328 ",
329         );
330     }
331
332     #[test]
333     fn continues_end_of_line_comment_with_space() {
334         cov_mark::check!(continues_end_of_line_comment_with_space);
335         do_check(
336             r#"
337 fn main() {
338     // Fix me $0
339     let x = 1 + 1;
340 }
341 "#,
342             r#"
343 fn main() {
344     // Fix me
345     // $0
346     let x = 1 + 1;
347 }
348 "#,
349         );
350     }
351
352     #[test]
353     fn trims_all_trailing_whitespace() {
354         do_check(
355             "
356 fn main() {
357     // Fix me  \t\t   $0
358     let x = 1 + 1;
359 }
360 ",
361             "
362 fn main() {
363     // Fix me
364     // $0
365     let x = 1 + 1;
366 }
367 ",
368         );
369     }
370
371     #[test]
372     fn indents_fn_body_block() {
373         cov_mark::check!(indent_block_contents);
374         do_check(
375             r#"
376 fn f() {$0()}
377         "#,
378             r#"
379 fn f() {
380     $0()
381 }
382         "#,
383         );
384     }
385
386     #[test]
387     fn indents_block_expr() {
388         do_check(
389             r#"
390 fn f() {
391     let x = {$0()};
392 }
393         "#,
394             r#"
395 fn f() {
396     let x = {
397         $0()
398     };
399 }
400         "#,
401         );
402     }
403
404     #[test]
405     fn indents_match_arm() {
406         do_check(
407             r#"
408 fn f() {
409     match 6 {
410         1 => {$0f()},
411         _ => (),
412     }
413 }
414         "#,
415             r#"
416 fn f() {
417     match 6 {
418         1 => {
419             $0f()
420         },
421         _ => (),
422     }
423 }
424         "#,
425         );
426     }
427
428     #[test]
429     fn indents_block_with_statement() {
430         do_check(
431             r#"
432 fn f() {$0a = b}
433         "#,
434             r#"
435 fn f() {
436     $0a = b
437 }
438         "#,
439         );
440         do_check(
441             r#"
442 fn f() {$0fn f() {}}
443         "#,
444             r#"
445 fn f() {
446     $0fn f() {}
447 }
448         "#,
449         );
450     }
451
452     #[test]
453     fn indents_nested_blocks() {
454         do_check(
455             r#"
456 fn f() {$0{}}
457         "#,
458             r#"
459 fn f() {
460     $0{}
461 }
462         "#,
463         );
464     }
465
466     #[test]
467     fn does_not_indent_empty_block() {
468         do_check_noop(
469             r#"
470 fn f() {$0}
471         "#,
472         );
473         do_check_noop(
474             r#"
475 fn f() {{$0}}
476         "#,
477         );
478     }
479
480     #[test]
481     fn does_not_indent_block_with_too_much_content() {
482         do_check_noop(
483             r#"
484 fn f() {$0 a = b; ()}
485         "#,
486         );
487         do_check_noop(
488             r#"
489 fn f() {$0 a = b; a = b; }
490         "#,
491         );
492     }
493
494     #[test]
495     fn does_not_indent_multiline_block() {
496         do_check_noop(
497             r#"
498 fn f() {$0
499 }
500         "#,
501         );
502         do_check_noop(
503             r#"
504 fn f() {$0
505
506 }
507         "#,
508         );
509     }
510
511     #[test]
512     fn indents_use_tree_list() {
513         do_check(
514             r#"
515 use crate::{$0};
516             "#,
517             r#"
518 use crate::{
519     $0
520 };
521             "#,
522         );
523         do_check(
524             r#"
525 use crate::{$0Object, path::to::OtherThing};
526             "#,
527             r#"
528 use crate::{
529     $0Object, path::to::OtherThing
530 };
531             "#,
532         );
533         do_check(
534             r#"
535 use {crate::{$0Object, path::to::OtherThing}};
536             "#,
537             r#"
538 use {crate::{
539     $0Object, path::to::OtherThing
540 }};
541             "#,
542         );
543         do_check(
544             r#"
545 use {
546     crate::{$0Object, path::to::OtherThing}
547 };
548             "#,
549             r#"
550 use {
551     crate::{
552         $0Object, path::to::OtherThing
553     }
554 };
555             "#,
556         );
557     }
558
559     #[test]
560     fn does_not_indent_use_tree_list_when_not_at_curly_brace() {
561         do_check_noop(
562             r#"
563 use path::{Thing$0};
564             "#,
565         );
566     }
567
568     #[test]
569     fn does_not_indent_use_tree_list_without_curly_braces() {
570         do_check_noop(
571             r#"
572 use path::Thing$0;
573             "#,
574         );
575         do_check_noop(
576             r#"
577 use path::$0Thing;
578             "#,
579         );
580         do_check_noop(
581             r#"
582 use path::Thing$0};
583             "#,
584         );
585         do_check_noop(
586             r#"
587 use path::{$0Thing;
588             "#,
589         );
590     }
591
592     #[test]
593     fn does_not_indent_multiline_use_tree_list() {
594         do_check_noop(
595             r#"
596 use path::{$0
597     Thing
598 };
599             "#,
600         );
601     }
602 }