]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing/on_enter.rs
Rename ra_ide -> ide
[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 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_last_space = 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_last_space = 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_last_space {
68         TextRange::new(position.offset - TextSize::of(' '), position.offset)
69     } else {
70         TextRange::empty(position.offset)
71     };
72     let edit = TextEdit::replace(delete, inserted);
73     Some(edit)
74 }
75
76 fn followed_by_comment(comment: &ast::Comment) -> bool {
77     let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) {
78         Some(it) => it,
79         None => return false,
80     };
81     if ws.spans_multiple_lines() {
82         return false;
83     }
84     ws.syntax().next_token().and_then(ast::Comment::cast).is_some()
85 }
86
87 fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
88     let ws = match file.syntax().token_at_offset(token.text_range().start()) {
89         TokenAtOffset::Between(l, r) => {
90             assert!(r == *token);
91             l
92         }
93         TokenAtOffset::Single(n) => {
94             assert!(n == *token);
95             return Some("".into());
96         }
97         TokenAtOffset::None => unreachable!(),
98     };
99     if ws.kind() != WHITESPACE {
100         return None;
101     }
102     let text = ws.text();
103     let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0);
104     Some(text[pos..].into())
105 }
106
107 #[cfg(test)]
108 mod tests {
109     use stdx::trim_indent;
110     use test_utils::{assert_eq_text, mark};
111
112     use crate::mock_analysis::analysis_and_position;
113
114     fn apply_on_enter(before: &str) -> Option<String> {
115         let (analysis, position) = analysis_and_position(&before);
116         let result = analysis.on_enter(position).unwrap()?;
117
118         let mut actual = analysis.file_text(position.file_id).unwrap().to_string();
119         result.apply(&mut actual);
120         Some(actual)
121     }
122
123     fn do_check(ra_fixture_before: &str, ra_fixture_after: &str) {
124         let ra_fixture_after = &trim_indent(ra_fixture_after);
125         let actual = apply_on_enter(ra_fixture_before).unwrap();
126         assert_eq_text!(ra_fixture_after, &actual);
127     }
128
129     fn do_check_noop(ra_fixture_text: &str) {
130         assert!(apply_on_enter(ra_fixture_text).is_none())
131     }
132
133     #[test]
134     fn continues_doc_comment() {
135         do_check(
136             r"
137 /// Some docs<|>
138 fn foo() {
139 }
140 ",
141             r"
142 /// Some docs
143 /// $0
144 fn foo() {
145 }
146 ",
147         );
148
149         do_check(
150             r"
151 impl S {
152     /// Some<|> docs.
153     fn foo() {}
154 }
155 ",
156             r"
157 impl S {
158     /// Some
159     /// $0 docs.
160     fn foo() {}
161 }
162 ",
163         );
164
165         do_check(
166             r"
167 ///<|> Some docs
168 fn foo() {
169 }
170 ",
171             r"
172 ///
173 /// $0 Some docs
174 fn foo() {
175 }
176 ",
177         );
178     }
179
180     #[test]
181     fn does_not_continue_before_doc_comment() {
182         do_check_noop(r"<|>//! docz");
183     }
184
185     #[test]
186     fn continues_code_comment_in_the_middle_of_line() {
187         do_check(
188             r"
189 fn main() {
190     // Fix<|> me
191     let x = 1 + 1;
192 }
193 ",
194             r"
195 fn main() {
196     // Fix
197     // $0 me
198     let x = 1 + 1;
199 }
200 ",
201         );
202     }
203
204     #[test]
205     fn continues_code_comment_in_the_middle_several_lines() {
206         do_check(
207             r"
208 fn main() {
209     // Fix<|>
210     // me
211     let x = 1 + 1;
212 }
213 ",
214             r"
215 fn main() {
216     // Fix
217     // $0
218     // me
219     let x = 1 + 1;
220 }
221 ",
222         );
223     }
224
225     #[test]
226     fn does_not_continue_end_of_line_comment() {
227         do_check_noop(
228             r"
229 fn main() {
230     // Fix me<|>
231     let x = 1 + 1;
232 }
233 ",
234         );
235     }
236
237     #[test]
238     fn continues_end_of_line_comment_with_space() {
239         mark::check!(continues_end_of_line_comment_with_space);
240         do_check(
241             r#"
242 fn main() {
243     // Fix me <|>
244     let x = 1 + 1;
245 }
246 "#,
247             r#"
248 fn main() {
249     // Fix me
250     // $0
251     let x = 1 + 1;
252 }
253 "#,
254         );
255     }
256 }