Skip to main content

ide/typing/
on_enter.rs

1//! Handles the `Enter` key press, including comment continuation and
2//! indentation in brace-delimited constructs.
3
4use ide_db::{FilePosition, RootDatabase, source_change::SnippetEdit};
5use syntax::{
6    AstNode, SmolStr, SourceFile,
7    SyntaxKind::*,
8    SyntaxToken, TextRange, TextSize, TokenAtOffset,
9    ast::{self, AstToken, edit::IndentLevel},
10};
11
12use ide_db::text_edit::TextEdit;
13
14// Feature: On Enter
15//
16// rust-analyzer can override <kbd>Enter</kbd> key to make it smarter:
17//
18// - <kbd>Enter</kbd> inside triple-slash comments automatically inserts `///`
19// - <kbd>Enter</kbd> in the middle or after a trailing space in `//` inserts `//`
20// - <kbd>Enter</kbd> inside `//!` doc comments automatically inserts `//!`
21// - <kbd>Enter</kbd> after `{` reformats single-line brace-delimited contents by
22//   moving the text between `{` and the matching `}` onto an indented line
23//
24// This action needs to be assigned to shortcut explicitly.
25//
26// Note that, depending on the other installed extensions, this feature can visibly slow down typing.
27// Similarly, if rust-analyzer crashes or stops responding, `Enter` might not work.
28// In that case, you can still press `Shift-Enter` to insert a newline.
29//
30// #### VS Code
31//
32// Add the following to `keybindings.json`:
33// ```json
34// {
35//   "key": "Enter",
36//   "command": "rust-analyzer.onEnter",
37//   "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust"
38// }
39// ````
40//
41// When using the Vim plugin:
42// ```json
43// {
44//   "key": "Enter",
45//   "command": "rust-analyzer.onEnter",
46//   "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust && vim.mode == 'Insert'"
47// }
48// ````
49//
50// ![On Enter](https://user-images.githubusercontent.com/48062697/113065578-04c21800-91b1-11eb-82b8-22b8c481e645.gif)
51pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
52    let editioned_file_id_wrapper =
53        ide_db::base_db::EditionedFileId::current_edition(db, position.file_id);
54    let parse = editioned_file_id_wrapper.parse(db);
55    let file = parse.tree();
56    let token = file.syntax().token_at_offset(position.offset).left_biased()?;
57
58    if let Some(comment) = ast::Comment::cast(token.clone()) {
59        return on_enter_in_comment(&comment, &file, position.offset);
60    }
61
62    if token.kind() == L_CURLY
63        && let Some(edit) = on_enter_in_braces(token, position)
64    {
65        cov_mark::hit!(indent_block_contents);
66        return Some(edit);
67    }
68
69    None
70}
71
72fn on_enter_in_comment(
73    comment: &ast::Comment,
74    file: &ast::SourceFile,
75    offset: TextSize,
76) -> Option<TextEdit> {
77    if comment.kind().shape.is_block() {
78        return None;
79    }
80
81    let prefix = comment.prefix();
82    let comment_range = comment.syntax().text_range();
83    if offset < comment_range.start() + TextSize::of(prefix) {
84        return None;
85    }
86
87    let mut remove_trailing_whitespace = false;
88    // Continuing single-line non-doc comments (like this one :) ) is annoying
89    if prefix == "//" && comment_range.end() == offset {
90        if comment.text().ends_with(' ') {
91            cov_mark::hit!(continues_end_of_line_comment_with_space);
92            remove_trailing_whitespace = true;
93        } else if !followed_by_comment(comment) {
94            return None;
95        }
96    }
97
98    let indent = node_indent(file, comment.syntax())?;
99    let inserted = format!("\n{indent}{prefix} $0");
100    let delete = if remove_trailing_whitespace {
101        let trimmed_len = comment.text().trim_end().len() as u32;
102        let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len;
103        TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset)
104    } else {
105        TextRange::empty(offset)
106    };
107    let edit = TextEdit::replace(delete, inserted);
108    Some(edit)
109}
110
111fn on_enter_in_braces(l_curly: SyntaxToken, position: FilePosition) -> Option<TextEdit> {
112    if l_curly.text_range().end() != position.offset {
113        return None;
114    }
115
116    let (r_curly, mut content) = brace_contents_on_same_line(&l_curly)?;
117    SnippetEdit::escape_snippet_bits(&mut content);
118    let indent = IndentLevel::from_token(&l_curly);
119    Some(TextEdit::replace(
120        TextRange::new(position.offset, r_curly.text_range().start()),
121        format!("\n{}$0{}\n{indent}", indent + 1, content),
122    ))
123}
124
125fn brace_contents_on_same_line(l_curly: &SyntaxToken) -> Option<(SyntaxToken, String)> {
126    let mut depth = 0_u32;
127    let mut tokens = Vec::new();
128    let mut token = l_curly.next_token()?;
129
130    loop {
131        if token.kind() == WHITESPACE && token.text().contains('\n') {
132            return None;
133        }
134
135        match token.kind() {
136            L_CURLY => {
137                depth += 1;
138                tokens.push(token.clone());
139            }
140            R_CURLY if depth == 0 => {
141                let first = tokens.iter().position(|it| it.kind() != WHITESPACE);
142                let last = tokens.iter().rposition(|it| it.kind() != WHITESPACE);
143                let content = match first.zip(last) {
144                    Some((first, last)) => {
145                        tokens[first..=last].iter().map(|it| it.text()).collect()
146                    }
147                    None => String::new(),
148                };
149                return Some((token, content));
150            }
151            R_CURLY => {
152                depth -= 1;
153                tokens.push(token.clone());
154            }
155            _ => tokens.push(token.clone()),
156        }
157
158        token = token.next_token()?;
159    }
160}
161
162fn 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
173fn 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)]
194mod 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(
210        #[rust_analyzer::rust_fixture] ra_fixture_before: &str,
211        #[rust_analyzer::rust_fixture] ra_fixture_after: &str,
212    ) {
213        let ra_fixture_after = &trim_indent(ra_fixture_after);
214        let actual = apply_on_enter(ra_fixture_before).unwrap();
215        assert_eq_text!(ra_fixture_after, &actual);
216    }
217
218    fn do_check_noop(ra_fixture_text: &str) {
219        assert!(apply_on_enter(ra_fixture_text).is_none())
220    }
221
222    #[test]
223    fn continues_doc_comment() {
224        do_check(
225            r"
226/// Some docs$0
227fn foo() {
228}
229",
230            r"
231/// Some docs
232/// $0
233fn foo() {
234}
235",
236        );
237
238        do_check(
239            r"
240impl S {
241    /// Some$0 docs.
242    fn foo() {}
243}
244",
245            r"
246impl S {
247    /// Some
248    /// $0 docs.
249    fn foo() {}
250}
251",
252        );
253
254        do_check(
255            r"
256///$0 Some docs
257fn foo() {
258}
259",
260            r"
261///
262/// $0 Some docs
263fn foo() {
264}
265",
266        );
267    }
268
269    #[test]
270    fn does_not_continue_before_doc_comment() {
271        do_check_noop(r"$0//! docz");
272    }
273
274    #[test]
275    fn continues_another_doc_comment() {
276        do_check(
277            r#"
278fn main() {
279    //! Documentation for$0 on enter
280    let x = 1 + 1;
281}
282"#,
283            r#"
284fn main() {
285    //! Documentation for
286    //! $0 on enter
287    let x = 1 + 1;
288}
289"#,
290        );
291    }
292
293    #[test]
294    fn continues_code_comment_in_the_middle_of_line() {
295        do_check(
296            r"
297fn main() {
298    // Fix$0 me
299    let x = 1 + 1;
300}
301",
302            r"
303fn main() {
304    // Fix
305    // $0 me
306    let x = 1 + 1;
307}
308",
309        );
310    }
311
312    #[test]
313    fn continues_code_comment_in_the_middle_several_lines() {
314        do_check(
315            r"
316fn main() {
317    // Fix$0
318    // me
319    let x = 1 + 1;
320}
321",
322            r"
323fn main() {
324    // Fix
325    // $0
326    // me
327    let x = 1 + 1;
328}
329",
330        );
331    }
332
333    #[test]
334    fn does_not_continue_end_of_line_comment() {
335        do_check_noop(
336            r"
337fn main() {
338    // Fix me$0
339    let x = 1 + 1;
340}
341",
342        );
343    }
344
345    #[test]
346    fn continues_end_of_line_comment_with_space() {
347        cov_mark::check!(continues_end_of_line_comment_with_space);
348        do_check(
349            r#"
350fn main() {
351    // Fix me $0
352    let x = 1 + 1;
353}
354"#,
355            r#"
356fn main() {
357    // Fix me
358    // $0
359    let x = 1 + 1;
360}
361"#,
362        );
363    }
364
365    #[test]
366    fn trims_all_trailing_whitespace() {
367        do_check(
368            "
369fn main() {
370    // Fix me  \t\t   $0
371    let x = 1 + 1;
372}
373",
374            "
375fn main() {
376    // Fix me
377    // $0
378    let x = 1 + 1;
379}
380",
381        );
382    }
383
384    #[test]
385    fn indents_empty_brace_pairs() {
386        cov_mark::check!(indent_block_contents);
387        do_check(
388            r#"
389fn f() {$0}
390        "#,
391            r#"
392fn f() {
393    $0
394}
395        "#,
396        );
397        do_check(
398            r#"
399fn f() {
400    let x = {$0};
401}
402        "#,
403            r#"
404fn f() {
405    let x = {
406        $0
407    };
408}
409        "#,
410        );
411        do_check(
412            r#"
413use crate::{$0};
414        "#,
415            r#"
416use crate::{
417    $0
418};
419        "#,
420        );
421        do_check(
422            r#"
423mod m {$0}
424            "#,
425            r#"
426mod m {
427    $0
428}
429            "#,
430        );
431    }
432
433    #[test]
434    fn indents_fn_body_block() {
435        do_check(
436            r#"
437fn f() {$0()}
438        "#,
439            r#"
440fn f() {
441    $0()
442}
443        "#,
444        );
445    }
446
447    #[test]
448    fn indents_block_expr() {
449        do_check(
450            r#"
451fn f() {
452    let x = {$0()};
453}
454        "#,
455            r#"
456fn f() {
457    let x = {
458        $0()
459    };
460}
461        "#,
462        );
463    }
464
465    #[test]
466    fn indents_match_arm() {
467        do_check(
468            r#"
469fn f() {
470    match 6 {
471        1 => {$0f()},
472        _ => (),
473    }
474}
475        "#,
476            r#"
477fn f() {
478    match 6 {
479        1 => {
480            $0f()
481        },
482        _ => (),
483    }
484}
485        "#,
486        );
487    }
488
489    #[test]
490    fn indents_block_with_statement() {
491        do_check(
492            r#"
493fn f() {$0a = b}
494        "#,
495            r#"
496fn f() {
497    $0a = b
498}
499        "#,
500        );
501        do_check(
502            r#"
503fn f() {$0fn f() {}}
504        "#,
505            r#"
506fn f() {
507    $0fn f() {}
508}
509        "#,
510        );
511    }
512
513    #[test]
514    fn indents_nested_blocks() {
515        do_check(
516            r#"
517fn f() {$0{}}
518        "#,
519            r#"
520fn f() {
521    $0{}
522}
523        "#,
524        );
525    }
526
527    #[test]
528    fn indents_block_with_multiple_statements() {
529        do_check(
530            r#"
531fn f() {$0 a = b; ()}
532        "#,
533            r#"
534fn f() {
535    $0a = b; ()
536}
537        "#,
538        );
539        do_check(
540            r#"
541fn f() {$0 a = b; a = b; }
542        "#,
543            r#"
544fn f() {
545    $0a = b; a = b;
546}
547        "#,
548        );
549    }
550
551    #[test]
552    fn trims_spaces_around_brace_contents() {
553        do_check(
554            r#"
555fn f() {$0   ()   }
556        "#,
557            r#"
558fn f() {
559    $0()
560}
561        "#,
562        );
563    }
564
565    #[test]
566    fn does_not_indent_multiline_block() {
567        do_check_noop(
568            r#"
569fn f() {$0
570}
571        "#,
572        );
573        do_check_noop(
574            r#"
575fn f() {$0
576
577}
578        "#,
579        );
580    }
581
582    #[test]
583    fn indents_use_tree_list() {
584        do_check(
585            r#"
586use crate::{$0};
587            "#,
588            r#"
589use crate::{
590    $0
591};
592            "#,
593        );
594        do_check(
595            r#"
596use crate::{$0Object, path::to::OtherThing};
597            "#,
598            r#"
599use crate::{
600    $0Object, path::to::OtherThing
601};
602            "#,
603        );
604        do_check(
605            r#"
606use {crate::{$0Object, path::to::OtherThing}};
607            "#,
608            r#"
609use {crate::{
610    $0Object, path::to::OtherThing
611}};
612            "#,
613        );
614        do_check(
615            r#"
616use {
617    crate::{$0Object, path::to::OtherThing}
618};
619            "#,
620            r#"
621use {
622    crate::{
623        $0Object, path::to::OtherThing
624    }
625};
626            "#,
627        );
628    }
629
630    #[test]
631    fn indents_item_lists() {
632        do_check(
633            r#"
634mod m {$0}
635            "#,
636            r#"
637mod m {
638    $0
639}
640            "#,
641        );
642    }
643
644    #[test]
645    fn does_not_indent_use_tree_list_when_not_at_curly_brace() {
646        do_check_noop(
647            r#"
648use path::{Thing$0};
649            "#,
650        );
651    }
652
653    #[test]
654    fn does_not_indent_use_tree_list_without_curly_braces() {
655        do_check_noop(
656            r#"
657use path::Thing$0;
658            "#,
659        );
660        do_check_noop(
661            r#"
662use path::$0Thing;
663            "#,
664        );
665        do_check_noop(
666            r#"
667use path::Thing$0};
668            "#,
669        );
670        do_check_noop(
671            r#"
672use path::{$0Thing;
673            "#,
674        );
675    }
676
677    #[test]
678    fn does_not_indent_multiline_use_tree_list() {
679        do_check_noop(
680            r#"
681use path::{$0
682    Thing
683};
684            "#,
685        );
686    }
687
688    #[test]
689    fn escapes_dollar_sign_in_brace_contents() {
690        do_check(
691            r#"
692fn f() {
693    const {$0$bar};
694}
695"#,
696            r#"
697fn f() {
698    const {
699        $0\$bar
700    };
701}
702"#,
703        );
704    }
705}