]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
Rewrite, reparse modified file
[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, Parse, SourceFile,
26     SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR},
27     TextRange, TextSize,
28 };
29
30 use text_edit::{Indel, 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);
63     assert_eq!(file.tree().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(
69     file: &Parse<SourceFile>,
70     offset: TextSize,
71     char_typed: char,
72 ) -> Option<TextEdit> {
73     assert!(TRIGGER_CHARS.contains(char_typed));
74     match char_typed {
75         '.' => on_dot_typed(&file.tree(), offset),
76         '=' => on_eq_typed(&file.tree(), offset),
77         '>' => on_arrow_typed(&file.tree(), offset),
78         '{' => on_opening_brace_typed(file, offset),
79         _ => unreachable!(),
80     }
81 }
82
83 /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
84 /// block.
85 fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> {
86     stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{'));
87
88     let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?;
89
90     // Remove the `{` to get a better parse tree, and reparse
91     let file = file.reparse(&Indel::delete(brace_token.text_range()));
92
93     let mut expr: ast::Expr = find_node_at_offset(file.tree().syntax(), offset)?;
94     if expr.syntax().text_range().start() != offset {
95         return None;
96     }
97
98     // Enclose the outermost expression starting at `offset`
99     while let Some(parent) = expr.syntax().parent() {
100         if parent.text_range().start() != expr.syntax().text_range().start() {
101             break;
102         }
103
104         match ast::Expr::cast(parent) {
105             Some(parent) => expr = parent,
106             None => break,
107         }
108     }
109
110     // If it's a statement in a block, we don't know how many statements should be included
111     if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) {
112         return None;
113     }
114
115     // Insert `}` right after the expression.
116     Some(TextEdit::insert(expr.syntax().text_range().end() + TextSize::of("{"), "}".to_string()))
117 }
118
119 /// Returns an edit which should be applied after `=` was typed. Primarily,
120 /// this works when adding `let =`.
121 // FIXME: use a snippet completion instead of this hack here.
122 fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
123     stdx::always!(file.syntax().text().char_at(offset) == Some('='));
124     let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
125     if let_stmt.semicolon_token().is_some() {
126         return None;
127     }
128     if let Some(expr) = let_stmt.initializer() {
129         let expr_range = expr.syntax().text_range();
130         if expr_range.contains(offset) && offset != expr_range.start() {
131             return None;
132         }
133         if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
134             return None;
135         }
136     } else {
137         return None;
138     }
139     let offset = let_stmt.syntax().text_range().end();
140     Some(TextEdit::insert(offset, ";".to_string()))
141 }
142
143 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
144 fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
145     stdx::always!(file.syntax().text().char_at(offset) == Some('.'));
146     let whitespace =
147         file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
148
149     let current_indent = {
150         let text = whitespace.text();
151         let newline = text.rfind('\n')?;
152         &text[newline + 1..]
153     };
154     let current_indent_len = TextSize::of(current_indent);
155
156     let parent = whitespace.syntax().parent()?;
157     // Make sure dot is a part of call chain
158     if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) {
159         return None;
160     }
161     let prev_indent = IndentLevel::from_node(&parent);
162     let target_indent = format!("    {}", prev_indent);
163     let target_indent_len = TextSize::of(&target_indent);
164     if current_indent_len == target_indent_len {
165         return None;
166     }
167
168     Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
169 }
170
171 /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
172 fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
173     let file_text = file.syntax().text();
174     stdx::always!(file_text.char_at(offset) == Some('>'));
175     let after_arrow = offset + TextSize::of('>');
176     if file_text.char_at(after_arrow) != Some('{') {
177         return None;
178     }
179     if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
180         return None;
181     }
182
183     Some(TextEdit::insert(after_arrow, " ".to_string()))
184 }
185
186 #[cfg(test)]
187 mod tests {
188     use test_utils::{assert_eq_text, extract_offset};
189
190     use super::*;
191
192     fn do_type_char(char_typed: char, before: &str) -> Option<String> {
193         let (offset, mut before) = extract_offset(before);
194         let edit = TextEdit::insert(offset, char_typed.to_string());
195         edit.apply(&mut before);
196         let parse = SourceFile::parse(&before);
197         on_char_typed_inner(&parse, offset, char_typed).map(|it| {
198             it.apply(&mut before);
199             before.to_string()
200         })
201     }
202
203     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
204         let actual = do_type_char(char_typed, ra_fixture_before)
205             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
206
207         assert_eq_text!(ra_fixture_after, &actual);
208     }
209
210     fn type_char_noop(char_typed: char, before: &str) {
211         let file_change = do_type_char(char_typed, before);
212         assert!(file_change.is_none())
213     }
214
215     #[test]
216     fn test_on_eq_typed() {
217         //     do_check(r"
218         // fn foo() {
219         //     let foo =$0
220         // }
221         // ", r"
222         // fn foo() {
223         //     let foo =;
224         // }
225         // ");
226         type_char(
227             '=',
228             r"
229 fn foo() {
230     let foo $0 1 + 1
231 }
232 ",
233             r"
234 fn foo() {
235     let foo = 1 + 1;
236 }
237 ",
238         );
239         //     do_check(r"
240         // fn foo() {
241         //     let foo =$0
242         //     let bar = 1;
243         // }
244         // ", r"
245         // fn foo() {
246         //     let foo =;
247         //     let bar = 1;
248         // }
249         // ");
250     }
251
252     #[test]
253     fn indents_new_chain_call() {
254         type_char(
255             '.',
256             r"
257             fn main() {
258                 xs.foo()
259                 $0
260             }
261             ",
262             r"
263             fn main() {
264                 xs.foo()
265                     .
266             }
267             ",
268         );
269         type_char_noop(
270             '.',
271             r"
272             fn main() {
273                 xs.foo()
274                     $0
275             }
276             ",
277         )
278     }
279
280     #[test]
281     fn indents_new_chain_call_with_semi() {
282         type_char(
283             '.',
284             r"
285             fn main() {
286                 xs.foo()
287                 $0;
288             }
289             ",
290             r"
291             fn main() {
292                 xs.foo()
293                     .;
294             }
295             ",
296         );
297         type_char_noop(
298             '.',
299             r"
300             fn main() {
301                 xs.foo()
302                     $0;
303             }
304             ",
305         )
306     }
307
308     #[test]
309     fn indents_new_chain_call_with_let() {
310         type_char(
311             '.',
312             r#"
313 fn main() {
314     let _ = foo
315     $0
316     bar()
317 }
318 "#,
319             r#"
320 fn main() {
321     let _ = foo
322         .
323     bar()
324 }
325 "#,
326         );
327     }
328
329     #[test]
330     fn indents_continued_chain_call() {
331         type_char(
332             '.',
333             r"
334             fn main() {
335                 xs.foo()
336                     .first()
337                 $0
338             }
339             ",
340             r"
341             fn main() {
342                 xs.foo()
343                     .first()
344                     .
345             }
346             ",
347         );
348         type_char_noop(
349             '.',
350             r"
351             fn main() {
352                 xs.foo()
353                     .first()
354                     $0
355             }
356             ",
357         );
358     }
359
360     #[test]
361     fn indents_middle_of_chain_call() {
362         type_char(
363             '.',
364             r"
365             fn source_impl() {
366                 let var = enum_defvariant_list().unwrap()
367                 $0
368                     .nth(92)
369                     .unwrap();
370             }
371             ",
372             r"
373             fn source_impl() {
374                 let var = enum_defvariant_list().unwrap()
375                     .
376                     .nth(92)
377                     .unwrap();
378             }
379             ",
380         );
381         type_char_noop(
382             '.',
383             r"
384             fn source_impl() {
385                 let var = enum_defvariant_list().unwrap()
386                     $0
387                     .nth(92)
388                     .unwrap();
389             }
390             ",
391         );
392     }
393
394     #[test]
395     fn dont_indent_freestanding_dot() {
396         type_char_noop(
397             '.',
398             r"
399             fn main() {
400                 $0
401             }
402             ",
403         );
404         type_char_noop(
405             '.',
406             r"
407             fn main() {
408             $0
409             }
410             ",
411         );
412     }
413
414     #[test]
415     fn adds_space_after_return_type() {
416         type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
417     }
418
419     #[test]
420     fn adds_closing_brace() {
421         type_char(
422             '{',
423             r#"
424 fn f() { match () { _ => $0() } }
425             "#,
426             r#"
427 fn f() { match () { _ => {()} } }
428             "#,
429         );
430         type_char(
431             '{',
432             r#"
433 fn f() { $0() }
434             "#,
435             r#"
436 fn f() { {()} }
437             "#,
438         );
439         type_char(
440             '{',
441             r#"
442 fn f() { let x = $0(); }
443             "#,
444             r#"
445 fn f() { let x = {()}; }
446             "#,
447         );
448         type_char(
449             '{',
450             r#"
451 fn f() { let x = $0a.b(); }
452             "#,
453             r#"
454 fn f() { let x = {a.b()}; }
455             "#,
456         );
457         type_char(
458             '{',
459             r#"
460 const S: () = $0();
461 fn f() {}
462             "#,
463             r#"
464 const S: () = {()};
465 fn f() {}
466             "#,
467         );
468         type_char(
469             '{',
470             r#"
471 const S: () = $0a.b();
472 fn f() {}
473             "#,
474             r#"
475 const S: () = {a.b()};
476 fn f() {}
477             "#,
478         );
479         type_char(
480             '{',
481             r#"
482 fn f() {
483     match x {
484         0 => $0(),
485         1 => (),
486     }
487 }
488             "#,
489             r#"
490 fn f() {
491     match x {
492         0 => {()},
493         1 => (),
494     }
495 }
496             "#,
497         );
498     }
499 }