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