]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing/on_enter.rs
Merge #8560
[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
59     None
60 }
61
62 fn on_enter_in_comment(
63     comment: &ast::Comment,
64     file: &ast::SourceFile,
65     offset: TextSize,
66 ) -> Option<TextEdit> {
67     if comment.kind().shape.is_block() {
68         return None;
69     }
70
71     let prefix = comment.prefix();
72     let comment_range = comment.syntax().text_range();
73     if offset < comment_range.start() + TextSize::of(prefix) {
74         return None;
75     }
76
77     let mut remove_trailing_whitespace = false;
78     // Continuing single-line non-doc comments (like this one :) ) is annoying
79     if prefix == "//" && comment_range.end() == offset {
80         if comment.text().ends_with(' ') {
81             cov_mark::hit!(continues_end_of_line_comment_with_space);
82             remove_trailing_whitespace = true;
83         } else if !followed_by_comment(&comment) {
84             return None;
85         }
86     }
87
88     let indent = node_indent(&file, comment.syntax())?;
89     let inserted = format!("\n{}{} $0", indent, prefix);
90     let delete = if remove_trailing_whitespace {
91         let trimmed_len = comment.text().trim_end().len() as u32;
92         let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len;
93         TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset)
94     } else {
95         TextRange::empty(offset)
96     };
97     let edit = TextEdit::replace(delete, inserted);
98     Some(edit)
99 }
100
101 fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<TextEdit> {
102     let contents = block_contents(&block)?;
103
104     if block.syntax().text().contains_char('\n') {
105         return None;
106     }
107
108     let indent = IndentLevel::from_node(block.syntax());
109     let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1));
110     edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{}", indent))).ok()?;
111     Some(edit)
112 }
113
114 fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> {
115     let mut node = block.tail_expr().map(|e| e.syntax().clone());
116
117     for stmt in block.statements() {
118         if node.is_some() {
119             // More than 1 node in the block
120             return None;
121         }
122
123         node = Some(stmt.syntax().clone());
124     }
125
126     node
127 }
128
129 fn followed_by_comment(comment: &ast::Comment) -> bool {
130     let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) {
131         Some(it) => it,
132         None => return false,
133     };
134     if ws.spans_multiple_lines() {
135         return false;
136     }
137     ws.syntax().next_token().and_then(ast::Comment::cast).is_some()
138 }
139
140 fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
141     let ws = match file.syntax().token_at_offset(token.text_range().start()) {
142         TokenAtOffset::Between(l, r) => {
143             assert!(r == *token);
144             l
145         }
146         TokenAtOffset::Single(n) => {
147             assert!(n == *token);
148             return Some("".into());
149         }
150         TokenAtOffset::None => unreachable!(),
151     };
152     if ws.kind() != WHITESPACE {
153         return None;
154     }
155     let text = ws.text();
156     let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0);
157     Some(text[pos..].into())
158 }
159
160 #[cfg(test)]
161 mod tests {
162     use stdx::trim_indent;
163     use test_utils::assert_eq_text;
164
165     use crate::fixture;
166
167     fn apply_on_enter(before: &str) -> Option<String> {
168         let (analysis, position) = fixture::position(&before);
169         let result = analysis.on_enter(position).unwrap()?;
170
171         let mut actual = analysis.file_text(position.file_id).unwrap().to_string();
172         result.apply(&mut actual);
173         Some(actual)
174     }
175
176     fn do_check(ra_fixture_before: &str, ra_fixture_after: &str) {
177         let ra_fixture_after = &trim_indent(ra_fixture_after);
178         let actual = apply_on_enter(ra_fixture_before).unwrap();
179         assert_eq_text!(ra_fixture_after, &actual);
180     }
181
182     fn do_check_noop(ra_fixture_text: &str) {
183         assert!(apply_on_enter(ra_fixture_text).is_none())
184     }
185
186     #[test]
187     fn continues_doc_comment() {
188         do_check(
189             r"
190 /// Some docs$0
191 fn foo() {
192 }
193 ",
194             r"
195 /// Some docs
196 /// $0
197 fn foo() {
198 }
199 ",
200         );
201
202         do_check(
203             r"
204 impl S {
205     /// Some$0 docs.
206     fn foo() {}
207 }
208 ",
209             r"
210 impl S {
211     /// Some
212     /// $0 docs.
213     fn foo() {}
214 }
215 ",
216         );
217
218         do_check(
219             r"
220 ///$0 Some docs
221 fn foo() {
222 }
223 ",
224             r"
225 ///
226 /// $0 Some docs
227 fn foo() {
228 }
229 ",
230         );
231     }
232
233     #[test]
234     fn does_not_continue_before_doc_comment() {
235         do_check_noop(r"$0//! docz");
236     }
237
238     #[test]
239     fn continues_another_doc_comment() {
240         do_check(
241             r#"
242 fn main() {
243     //! Documentation for$0 on enter
244     let x = 1 + 1;
245 }
246 "#,
247             r#"
248 fn main() {
249     //! Documentation for
250     //! $0 on enter
251     let x = 1 + 1;
252 }
253 "#,
254         );
255     }
256
257     #[test]
258     fn continues_code_comment_in_the_middle_of_line() {
259         do_check(
260             r"
261 fn main() {
262     // Fix$0 me
263     let x = 1 + 1;
264 }
265 ",
266             r"
267 fn main() {
268     // Fix
269     // $0 me
270     let x = 1 + 1;
271 }
272 ",
273         );
274     }
275
276     #[test]
277     fn continues_code_comment_in_the_middle_several_lines() {
278         do_check(
279             r"
280 fn main() {
281     // Fix$0
282     // me
283     let x = 1 + 1;
284 }
285 ",
286             r"
287 fn main() {
288     // Fix
289     // $0
290     // me
291     let x = 1 + 1;
292 }
293 ",
294         );
295     }
296
297     #[test]
298     fn does_not_continue_end_of_line_comment() {
299         do_check_noop(
300             r"
301 fn main() {
302     // Fix me$0
303     let x = 1 + 1;
304 }
305 ",
306         );
307     }
308
309     #[test]
310     fn continues_end_of_line_comment_with_space() {
311         cov_mark::check!(continues_end_of_line_comment_with_space);
312         do_check(
313             r#"
314 fn main() {
315     // Fix me $0
316     let x = 1 + 1;
317 }
318 "#,
319             r#"
320 fn main() {
321     // Fix me
322     // $0
323     let x = 1 + 1;
324 }
325 "#,
326         );
327     }
328
329     #[test]
330     fn trims_all_trailing_whitespace() {
331         do_check(
332             "
333 fn main() {
334     // Fix me  \t\t   $0
335     let x = 1 + 1;
336 }
337 ",
338             "
339 fn main() {
340     // Fix me
341     // $0
342     let x = 1 + 1;
343 }
344 ",
345         );
346     }
347
348     #[test]
349     fn indents_fn_body_block() {
350         cov_mark::check!(indent_block_contents);
351         do_check(
352             r#"
353 fn f() {$0()}
354         "#,
355             r#"
356 fn f() {
357     $0()
358 }
359         "#,
360         );
361     }
362
363     #[test]
364     fn indents_block_expr() {
365         do_check(
366             r#"
367 fn f() {
368     let x = {$0()};
369 }
370         "#,
371             r#"
372 fn f() {
373     let x = {
374         $0()
375     };
376 }
377         "#,
378         );
379     }
380
381     #[test]
382     fn indents_match_arm() {
383         do_check(
384             r#"
385 fn f() {
386     match 6 {
387         1 => {$0f()},
388         _ => (),
389     }
390 }
391         "#,
392             r#"
393 fn f() {
394     match 6 {
395         1 => {
396             $0f()
397         },
398         _ => (),
399     }
400 }
401         "#,
402         );
403     }
404
405     #[test]
406     fn indents_block_with_statement() {
407         do_check(
408             r#"
409 fn f() {$0a = b}
410         "#,
411             r#"
412 fn f() {
413     $0a = b
414 }
415         "#,
416         );
417         do_check(
418             r#"
419 fn f() {$0fn f() {}}
420         "#,
421             r#"
422 fn f() {
423     $0fn f() {}
424 }
425         "#,
426         );
427     }
428
429     #[test]
430     fn indents_nested_blocks() {
431         do_check(
432             r#"
433 fn f() {$0{}}
434         "#,
435             r#"
436 fn f() {
437     $0{}
438 }
439         "#,
440         );
441     }
442
443     #[test]
444     fn does_not_indent_empty_block() {
445         do_check_noop(
446             r#"
447 fn f() {$0}
448         "#,
449         );
450         do_check_noop(
451             r#"
452 fn f() {{$0}}
453         "#,
454         );
455     }
456
457     #[test]
458     fn does_not_indent_block_with_too_much_content() {
459         do_check_noop(
460             r#"
461 fn f() {$0 a = b; ()}
462         "#,
463         );
464         do_check_noop(
465             r#"
466 fn f() {$0 a = b; a = b; }
467         "#,
468         );
469     }
470
471     #[test]
472     fn does_not_indent_multiline_block() {
473         do_check_noop(
474             r#"
475 fn f() {$0
476 }
477         "#,
478         );
479         do_check_noop(
480             r#"
481 fn f() {$0
482
483 }
484         "#,
485         );
486     }
487 }