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