]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
Autoclose blocks when typing `{`
[rust.git] / crates / ide / src / typing.rs
1 //! This module handles auto-magic editing actions applied together with users
2 //! edits. For example, if the user typed
3 //!
4 //! ```text
5 //!     foo
6 //!         .bar()
7 //!         .baz()
8 //!     |   // <- cursor is here
9 //! ```
10 //!
11 //! and types `.` next, we want to indent the dot.
12 //!
13 //! Language server executes such typing assists synchronously. That is, they
14 //! block user's typing and should be pretty fast for this reason!
15
16 mod on_enter;
17
18 use ide_db::{
19     base_db::{FilePosition, SourceDatabase},
20     RootDatabase,
21 };
22 use syntax::{
23     algo::find_node_at_offset,
24     ast::{self, edit::IndentLevel, AstToken},
25     AstNode, SourceFile,
26     SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR},
27     TextRange, TextSize,
28 };
29
30 use text_edit::TextEdit;
31
32 use crate::SourceChange;
33
34 pub(crate) use on_enter::on_enter;
35
36 // Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`.
37 pub(crate) const TRIGGER_CHARS: &str = ".=>{";
38
39 // Feature: On Typing Assists
40 //
41 // Some features trigger on typing certain characters:
42 //
43 // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
44 // - typing `.` in a chain method call auto-indents
45 //
46 // VS Code::
47 //
48 // Add the following to `settings.json`:
49 // [source,json]
50 // ----
51 // "editor.formatOnType": true,
52 // ----
53 //
54 // image::https://user-images.githubusercontent.com/48062697/113166163-69758500-923a-11eb-81ee-eb33ec380399.gif[]
55 // image::https://user-images.githubusercontent.com/48062697/113171066-105c2000-923f-11eb-87ab-f4a263346567.gif[]
56 pub(crate) fn on_char_typed(
57     db: &RootDatabase,
58     position: FilePosition,
59     char_typed: char,
60 ) -> Option<SourceChange> {
61     assert!(TRIGGER_CHARS.contains(char_typed));
62     let file = &db.parse(position.file_id).tree();
63     assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
64     let edit = on_char_typed_inner(file, position.offset, char_typed)?;
65     Some(SourceChange::from_text_edit(position.file_id, edit))
66 }
67
68 fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> {
69     assert!(TRIGGER_CHARS.contains(char_typed));
70     match char_typed {
71         '.' => on_dot_typed(file, offset),
72         '=' => on_eq_typed(file, offset),
73         '>' => on_arrow_typed(file, offset),
74         '{' => on_opening_brace_typed(file, offset),
75         _ => unreachable!(),
76     }
77 }
78
79 /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
80 /// block.
81 fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
82     assert_eq!(file.syntax().text().char_at(offset), Some('{'));
83     let brace_token = file.syntax().token_at_offset(offset).right_biased()?;
84     let block = ast::BlockExpr::cast(brace_token.parent()?)?;
85
86     // We expect a block expression enclosing exactly 1 preexisting expression. It can be parsed as
87     // either the trailing expr or an ExprStmt.
88     let offset = {
89         match block.tail_expr() {
90             Some(expr) => {
91                 if block.statements().next().is_some() {
92                     return None;
93                 }
94                 expr.syntax().text_range().end()
95             }
96             None => {
97                 if block.statements().count() != 1 {
98                     return None;
99                 }
100
101                 match block.statements().next()? {
102                     ast::Stmt::ExprStmt(it) => {
103                         // Use the expression span to place `}` before the `;`
104                         it.expr()?.syntax().text_range().end()
105                     }
106                     _ => return None,
107                 }
108             }
109         }
110     };
111
112     Some(TextEdit::insert(offset, "}".to_string()))
113 }
114
115 /// Returns an edit which should be applied after `=` was typed. Primarily,
116 /// this works when adding `let =`.
117 // FIXME: use a snippet completion instead of this hack here.
118 fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
119     assert_eq!(file.syntax().text().char_at(offset), Some('='));
120     let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
121     if let_stmt.semicolon_token().is_some() {
122         return None;
123     }
124     if let Some(expr) = let_stmt.initializer() {
125         let expr_range = expr.syntax().text_range();
126         if expr_range.contains(offset) && offset != expr_range.start() {
127             return None;
128         }
129         if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
130             return None;
131         }
132     } else {
133         return None;
134     }
135     let offset = let_stmt.syntax().text_range().end();
136     Some(TextEdit::insert(offset, ";".to_string()))
137 }
138
139 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
140 fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
141     assert_eq!(file.syntax().text().char_at(offset), Some('.'));
142     let whitespace =
143         file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
144
145     let current_indent = {
146         let text = whitespace.text();
147         let newline = text.rfind('\n')?;
148         &text[newline + 1..]
149     };
150     let current_indent_len = TextSize::of(current_indent);
151
152     let parent = whitespace.syntax().parent()?;
153     // Make sure dot is a part of call chain
154     if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) {
155         return None;
156     }
157     let prev_indent = IndentLevel::from_node(&parent);
158     let target_indent = format!("    {}", prev_indent);
159     let target_indent_len = TextSize::of(&target_indent);
160     if current_indent_len == target_indent_len {
161         return None;
162     }
163
164     Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
165 }
166
167 /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
168 fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
169     let file_text = file.syntax().text();
170     assert_eq!(file_text.char_at(offset), Some('>'));
171     let after_arrow = offset + TextSize::of('>');
172     if file_text.char_at(after_arrow) != Some('{') {
173         return None;
174     }
175     if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
176         return None;
177     }
178
179     Some(TextEdit::insert(after_arrow, " ".to_string()))
180 }
181
182 #[cfg(test)]
183 mod tests {
184     use test_utils::{assert_eq_text, extract_offset};
185
186     use super::*;
187
188     fn do_type_char(char_typed: char, before: &str) -> Option<String> {
189         let (offset, mut before) = extract_offset(before);
190         let edit = TextEdit::insert(offset, char_typed.to_string());
191         edit.apply(&mut before);
192         let parse = SourceFile::parse(&before);
193         on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| {
194             it.apply(&mut before);
195             before.to_string()
196         })
197     }
198
199     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
200         let actual = do_type_char(char_typed, ra_fixture_before)
201             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
202
203         assert_eq_text!(ra_fixture_after, &actual);
204     }
205
206     fn type_char_noop(char_typed: char, before: &str) {
207         let file_change = do_type_char(char_typed, before);
208         assert!(file_change.is_none())
209     }
210
211     #[test]
212     fn test_on_eq_typed() {
213         //     do_check(r"
214         // fn foo() {
215         //     let foo =$0
216         // }
217         // ", r"
218         // fn foo() {
219         //     let foo =;
220         // }
221         // ");
222         type_char(
223             '=',
224             r"
225 fn foo() {
226     let foo $0 1 + 1
227 }
228 ",
229             r"
230 fn foo() {
231     let foo = 1 + 1;
232 }
233 ",
234         );
235         //     do_check(r"
236         // fn foo() {
237         //     let foo =$0
238         //     let bar = 1;
239         // }
240         // ", r"
241         // fn foo() {
242         //     let foo =;
243         //     let bar = 1;
244         // }
245         // ");
246     }
247
248     #[test]
249     fn indents_new_chain_call() {
250         type_char(
251             '.',
252             r"
253             fn main() {
254                 xs.foo()
255                 $0
256             }
257             ",
258             r"
259             fn main() {
260                 xs.foo()
261                     .
262             }
263             ",
264         );
265         type_char_noop(
266             '.',
267             r"
268             fn main() {
269                 xs.foo()
270                     $0
271             }
272             ",
273         )
274     }
275
276     #[test]
277     fn indents_new_chain_call_with_semi() {
278         type_char(
279             '.',
280             r"
281             fn main() {
282                 xs.foo()
283                 $0;
284             }
285             ",
286             r"
287             fn main() {
288                 xs.foo()
289                     .;
290             }
291             ",
292         );
293         type_char_noop(
294             '.',
295             r"
296             fn main() {
297                 xs.foo()
298                     $0;
299             }
300             ",
301         )
302     }
303
304     #[test]
305     fn indents_new_chain_call_with_let() {
306         type_char(
307             '.',
308             r#"
309 fn main() {
310     let _ = foo
311     $0
312     bar()
313 }
314 "#,
315             r#"
316 fn main() {
317     let _ = foo
318         .
319     bar()
320 }
321 "#,
322         );
323     }
324
325     #[test]
326     fn indents_continued_chain_call() {
327         type_char(
328             '.',
329             r"
330             fn main() {
331                 xs.foo()
332                     .first()
333                 $0
334             }
335             ",
336             r"
337             fn main() {
338                 xs.foo()
339                     .first()
340                     .
341             }
342             ",
343         );
344         type_char_noop(
345             '.',
346             r"
347             fn main() {
348                 xs.foo()
349                     .first()
350                     $0
351             }
352             ",
353         );
354     }
355
356     #[test]
357     fn indents_middle_of_chain_call() {
358         type_char(
359             '.',
360             r"
361             fn source_impl() {
362                 let var = enum_defvariant_list().unwrap()
363                 $0
364                     .nth(92)
365                     .unwrap();
366             }
367             ",
368             r"
369             fn source_impl() {
370                 let var = enum_defvariant_list().unwrap()
371                     .
372                     .nth(92)
373                     .unwrap();
374             }
375             ",
376         );
377         type_char_noop(
378             '.',
379             r"
380             fn source_impl() {
381                 let var = enum_defvariant_list().unwrap()
382                     $0
383                     .nth(92)
384                     .unwrap();
385             }
386             ",
387         );
388     }
389
390     #[test]
391     fn dont_indent_freestanding_dot() {
392         type_char_noop(
393             '.',
394             r"
395             fn main() {
396                 $0
397             }
398             ",
399         );
400         type_char_noop(
401             '.',
402             r"
403             fn main() {
404             $0
405             }
406             ",
407         );
408     }
409
410     #[test]
411     fn adds_space_after_return_type() {
412         type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
413     }
414
415     #[test]
416     fn adds_closing_brace() {
417         type_char('{', r"fn f() { match () { _ => $0() } }", r"fn f() { match () { _ => {()} } }");
418         type_char('{', r"fn f() { $0(); }", r"fn f() { {()}; }");
419         type_char('{', r"fn f() { let x = $0(); }", r"fn f() { let x = {()}; }");
420     }
421 }