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