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