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