]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing/on_enter.rs
Merge #7195
[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     ast::{self, AstToken},
8     AstNode, SmolStr, SourceFile,
9     SyntaxKind::*,
10     SyntaxToken, TextRange, TextSize, TokenAtOffset,
11 };
12 use test_utils::mark;
13 use text_edit::TextEdit;
14
15 // Feature: On Enter
16 //
17 // rust-analyzer can override kbd:[Enter] key to make it smarter:
18 //
19 // - kbd:[Enter] inside triple-slash comments automatically inserts `///`
20 // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//`
21 //
22 // This action needs to be assigned to shortcut explicitly.
23 //
24 // VS Code::
25 //
26 // Add the following to `keybindings.json`:
27 // [source,json]
28 // ----
29 // {
30 //   "key": "Enter",
31 //   "command": "rust-analyzer.onEnter",
32 //   "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust"
33 // }
34 // ----
35 pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
36     let parse = db.parse(position.file_id);
37     let file = parse.tree();
38     let comment = file
39         .syntax()
40         .token_at_offset(position.offset)
41         .left_biased()
42         .and_then(ast::Comment::cast)?;
43
44     if comment.kind().shape.is_block() {
45         return None;
46     }
47
48     let prefix = comment.prefix();
49     let comment_range = comment.syntax().text_range();
50     if position.offset < comment_range.start() + TextSize::of(prefix) {
51         return None;
52     }
53
54     let mut remove_trailing_whitespace = false;
55     // Continuing single-line non-doc comments (like this one :) ) is annoying
56     if prefix == "//" && comment_range.end() == position.offset {
57         if comment.text().ends_with(' ') {
58             mark::hit!(continues_end_of_line_comment_with_space);
59             remove_trailing_whitespace = true;
60         } else if !followed_by_comment(&comment) {
61             return None;
62         }
63     }
64
65     let indent = node_indent(&file, comment.syntax())?;
66     let inserted = format!("\n{}{} $0", indent, prefix);
67     let delete = if remove_trailing_whitespace {
68         let trimmed_len = comment.text().trim_end().len() as u32;
69         let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len;
70         TextRange::new(position.offset - TextSize::from(trailing_whitespace_len), position.offset)
71     } else {
72         TextRange::empty(position.offset)
73     };
74     let edit = TextEdit::replace(delete, inserted);
75     Some(edit)
76 }
77
78 fn followed_by_comment(comment: &ast::Comment) -> bool {
79     let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) {
80         Some(it) => it,
81         None => return false,
82     };
83     if ws.spans_multiple_lines() {
84         return false;
85     }
86     ws.syntax().next_token().and_then(ast::Comment::cast).is_some()
87 }
88
89 fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
90     let ws = match file.syntax().token_at_offset(token.text_range().start()) {
91         TokenAtOffset::Between(l, r) => {
92             assert!(r == *token);
93             l
94         }
95         TokenAtOffset::Single(n) => {
96             assert!(n == *token);
97             return Some("".into());
98         }
99         TokenAtOffset::None => unreachable!(),
100     };
101     if ws.kind() != WHITESPACE {
102         return None;
103     }
104     let text = ws.text();
105     let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0);
106     Some(text[pos..].into())
107 }
108
109 #[cfg(test)]
110 mod tests {
111     use stdx::trim_indent;
112     use test_utils::{assert_eq_text, mark};
113
114     use crate::fixture;
115
116     fn apply_on_enter(before: &str) -> Option<String> {
117         let (analysis, position) = fixture::position(&before);
118         let result = analysis.on_enter(position).unwrap()?;
119
120         let mut actual = analysis.file_text(position.file_id).unwrap().to_string();
121         result.apply(&mut actual);
122         Some(actual)
123     }
124
125     fn do_check(ra_fixture_before: &str, ra_fixture_after: &str) {
126         let ra_fixture_after = &trim_indent(ra_fixture_after);
127         let actual = apply_on_enter(ra_fixture_before).unwrap();
128         assert_eq_text!(ra_fixture_after, &actual);
129     }
130
131     fn do_check_noop(ra_fixture_text: &str) {
132         assert!(apply_on_enter(ra_fixture_text).is_none())
133     }
134
135     #[test]
136     fn continues_doc_comment() {
137         do_check(
138             r"
139 /// Some docs$0
140 fn foo() {
141 }
142 ",
143             r"
144 /// Some docs
145 /// $0
146 fn foo() {
147 }
148 ",
149         );
150
151         do_check(
152             r"
153 impl S {
154     /// Some$0 docs.
155     fn foo() {}
156 }
157 ",
158             r"
159 impl S {
160     /// Some
161     /// $0 docs.
162     fn foo() {}
163 }
164 ",
165         );
166
167         do_check(
168             r"
169 ///$0 Some docs
170 fn foo() {
171 }
172 ",
173             r"
174 ///
175 /// $0 Some docs
176 fn foo() {
177 }
178 ",
179         );
180     }
181
182     #[test]
183     fn does_not_continue_before_doc_comment() {
184         do_check_noop(r"$0//! docz");
185     }
186
187     #[test]
188     fn continues_code_comment_in_the_middle_of_line() {
189         do_check(
190             r"
191 fn main() {
192     // Fix$0 me
193     let x = 1 + 1;
194 }
195 ",
196             r"
197 fn main() {
198     // Fix
199     // $0 me
200     let x = 1 + 1;
201 }
202 ",
203         );
204     }
205
206     #[test]
207     fn continues_code_comment_in_the_middle_several_lines() {
208         do_check(
209             r"
210 fn main() {
211     // Fix$0
212     // me
213     let x = 1 + 1;
214 }
215 ",
216             r"
217 fn main() {
218     // Fix
219     // $0
220     // me
221     let x = 1 + 1;
222 }
223 ",
224         );
225     }
226
227     #[test]
228     fn does_not_continue_end_of_line_comment() {
229         do_check_noop(
230             r"
231 fn main() {
232     // Fix me$0
233     let x = 1 + 1;
234 }
235 ",
236         );
237     }
238
239     #[test]
240     fn continues_end_of_line_comment_with_space() {
241         mark::check!(continues_end_of_line_comment_with_space);
242         do_check(
243             r#"
244 fn main() {
245     // Fix me $0
246     let x = 1 + 1;
247 }
248 "#,
249             r#"
250 fn main() {
251     // Fix me
252     // $0
253     let x = 1 + 1;
254 }
255 "#,
256         );
257     }
258
259     #[test]
260     fn trims_all_trailing_whitespace() {
261         do_check(
262             "
263 fn main() {
264     // Fix me  \t\t   $0
265     let x = 1 + 1;
266 }
267 ",
268             "
269 fn main() {
270     // Fix me
271     // $0
272     let x = 1 + 1;
273 }
274 ",
275         );
276     }
277 }