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::{self, 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[]
on_char_typed( db: &RootDatabase, position: FilePosition, char_typed: char, ) -> Option<SourceChange>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 
on_char_typed_inner( file: &Parse<SourceFile>, offset: TextSize, char_typed: char, ) -> Option<TextEdit>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, or a part of a `use` item.
on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit>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     if brace_token.kind() != SyntaxKind::L_CURLY {
99         return None;
100     }
101 
102     // Remove the `{` to get a better parse tree, and reparse.
103     let range = brace_token.text_range();
104     if !stdx::always!(range.len() == TextSize::of('{')) {
105         return None;
106     }
107     let file = file.reparse(&Indel::delete(range));
108 
109     if let Some(edit) = brace_expr(&file.tree(), offset) {
110         return Some(edit);
111     }
112 
113     if let Some(edit) = brace_use_path(&file.tree(), offset) {
114         return Some(edit);
115     }
116 
117     return None;
118 
119     fn brace_use_path(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
120         let segment: ast::PathSegment = find_node_at_offset(file.syntax(), offset)?;
121         if segment.syntax().text_range().start() != offset {
122             return None;
123         }
124 
125         let tree: ast::UseTree = find_node_at_offset(file.syntax(), offset)?;
126 
127         Some(TextEdit::insert(
128             tree.syntax().text_range().end() + TextSize::of("{"),
129             "}".to_string(),
130         ))
131     }
132 
133     fn brace_expr(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
134         let mut expr: ast::Expr = find_node_at_offset(file.syntax(), offset)?;
135         if expr.syntax().text_range().start() != offset {
136             return None;
137         }
138 
139         // Enclose the outermost expression starting at `offset`
140         while let Some(parent) = expr.syntax().parent() {
141             if parent.text_range().start() != expr.syntax().text_range().start() {
142                 break;
143             }
144 
145             match ast::Expr::cast(parent) {
146                 Some(parent) => expr = parent,
147                 None => break,
148             }
149         }
150 
151         // If it's a statement in a block, we don't know how many statements should be included
152         if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) {
153             return None;
154         }
155 
156         // Insert `}` right after the expression.
157         Some(TextEdit::insert(
158             expr.syntax().text_range().end() + TextSize::of("{"),
159             "}".to_string(),
160         ))
161     }
162 }
163 
164 /// Returns an edit which should be applied after `=` was typed. Primarily,
165 /// this works when adding `let =`.
166 // FIXME: use a snippet completion instead of this hack here.
on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit>167 fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
168     if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) {
169         return None;
170     }
171     let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
172     if let_stmt.semicolon_token().is_some() {
173         return None;
174     }
175     if let Some(expr) = let_stmt.initializer() {
176         let expr_range = expr.syntax().text_range();
177         if expr_range.contains(offset) && offset != expr_range.start() {
178             return None;
179         }
180         if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
181             return None;
182         }
183     } else {
184         return None;
185     }
186     let offset = let_stmt.syntax().text_range().end();
187     Some(TextEdit::insert(offset, ";".to_string()))
188 }
189 
190 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit>191 fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
192     if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) {
193         return None;
194     }
195     let whitespace =
196         file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
197 
198     let current_indent = {
199         let text = whitespace.text();
200         let newline = text.rfind('\n')?;
201         &text[newline + 1..]
202     };
203     let current_indent_len = TextSize::of(current_indent);
204 
205     let parent = whitespace.syntax().parent()?;
206     // Make sure dot is a part of call chain
207     if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) {
208         return None;
209     }
210     let prev_indent = IndentLevel::from_node(&parent);
211     let target_indent = format!("    {}", prev_indent);
212     let target_indent_len = TextSize::of(&target_indent);
213     if current_indent_len == target_indent_len {
214         return None;
215     }
216 
217     Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
218 }
219 
220 /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit>221 fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
222     let file_text = file.syntax().text();
223     if !stdx::always!(file_text.char_at(offset) == Some('>')) {
224         return None;
225     }
226     let after_arrow = offset + TextSize::of('>');
227     if file_text.char_at(after_arrow) != Some('{') {
228         return None;
229     }
230     if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
231         return None;
232     }
233 
234     Some(TextEdit::insert(after_arrow, " ".to_string()))
235 }
236 
237 #[cfg(test)]
238 mod tests {
239     use test_utils::{assert_eq_text, extract_offset};
240 
241     use super::*;
242 
do_type_char(char_typed: char, before: &str) -> Option<String>243     fn do_type_char(char_typed: char, before: &str) -> Option<String> {
244         let (offset, mut before) = extract_offset(before);
245         let edit = TextEdit::insert(offset, char_typed.to_string());
246         edit.apply(&mut before);
247         let parse = SourceFile::parse(&before);
248         on_char_typed_inner(&parse, offset, char_typed).map(|it| {
249             it.apply(&mut before);
250             before.to_string()
251         })
252     }
253 
type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str)254     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
255         let actual = do_type_char(char_typed, ra_fixture_before)
256             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
257 
258         assert_eq_text!(ra_fixture_after, &actual);
259     }
260 
type_char_noop(char_typed: char, ra_fixture_before: &str)261     fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
262         let file_change = do_type_char(char_typed, ra_fixture_before);
263         assert!(file_change.is_none())
264     }
265 
266     #[test]
test_on_eq_typed()267     fn test_on_eq_typed() {
268         //     do_check(r"
269         // fn foo() {
270         //     let foo =$0
271         // }
272         // ", r"
273         // fn foo() {
274         //     let foo =;
275         // }
276         // ");
277         type_char(
278             '=',
279             r#"
280 fn foo() {
281     let foo $0 1 + 1
282 }
283 "#,
284             r#"
285 fn foo() {
286     let foo = 1 + 1;
287 }
288 "#,
289         );
290         //     do_check(r"
291         // fn foo() {
292         //     let foo =$0
293         //     let bar = 1;
294         // }
295         // ", r"
296         // fn foo() {
297         //     let foo =;
298         //     let bar = 1;
299         // }
300         // ");
301     }
302 
303     #[test]
indents_new_chain_call()304     fn indents_new_chain_call() {
305         type_char(
306             '.',
307             r#"
308 fn main() {
309     xs.foo()
310     $0
311 }
312             "#,
313             r#"
314 fn main() {
315     xs.foo()
316         .
317 }
318             "#,
319         );
320         type_char_noop(
321             '.',
322             r#"
323 fn main() {
324     xs.foo()
325         $0
326 }
327             "#,
328         )
329     }
330 
331     #[test]
indents_new_chain_call_with_semi()332     fn indents_new_chain_call_with_semi() {
333         type_char(
334             '.',
335             r"
336 fn main() {
337     xs.foo()
338     $0;
339 }
340             ",
341             r#"
342 fn main() {
343     xs.foo()
344         .;
345 }
346             "#,
347         );
348         type_char_noop(
349             '.',
350             r#"
351 fn main() {
352     xs.foo()
353         $0;
354 }
355             "#,
356         )
357     }
358 
359     #[test]
indents_new_chain_call_with_let()360     fn indents_new_chain_call_with_let() {
361         type_char(
362             '.',
363             r#"
364 fn main() {
365     let _ = foo
366     $0
367     bar()
368 }
369 "#,
370             r#"
371 fn main() {
372     let _ = foo
373         .
374     bar()
375 }
376 "#,
377         );
378     }
379 
380     #[test]
indents_continued_chain_call()381     fn indents_continued_chain_call() {
382         type_char(
383             '.',
384             r#"
385 fn main() {
386     xs.foo()
387         .first()
388     $0
389 }
390             "#,
391             r#"
392 fn main() {
393     xs.foo()
394         .first()
395         .
396 }
397             "#,
398         );
399         type_char_noop(
400             '.',
401             r#"
402 fn main() {
403     xs.foo()
404         .first()
405         $0
406 }
407             "#,
408         );
409     }
410 
411     #[test]
indents_middle_of_chain_call()412     fn indents_middle_of_chain_call() {
413         type_char(
414             '.',
415             r#"
416 fn source_impl() {
417     let var = enum_defvariant_list().unwrap()
418     $0
419         .nth(92)
420         .unwrap();
421 }
422             "#,
423             r#"
424 fn source_impl() {
425     let var = enum_defvariant_list().unwrap()
426         .
427         .nth(92)
428         .unwrap();
429 }
430             "#,
431         );
432         type_char_noop(
433             '.',
434             r#"
435 fn source_impl() {
436     let var = enum_defvariant_list().unwrap()
437         $0
438         .nth(92)
439         .unwrap();
440 }
441             "#,
442         );
443     }
444 
445     #[test]
dont_indent_freestanding_dot()446     fn dont_indent_freestanding_dot() {
447         type_char_noop(
448             '.',
449             r#"
450 fn main() {
451     $0
452 }
453             "#,
454         );
455         type_char_noop(
456             '.',
457             r#"
458 fn main() {
459 $0
460 }
461             "#,
462         );
463     }
464 
465     #[test]
adds_space_after_return_type()466     fn adds_space_after_return_type() {
467         type_char(
468             '>',
469             r#"
470 fn foo() -$0{ 92 }
471 "#,
472             r#"
473 fn foo() -> { 92 }
474 "#,
475         );
476     }
477 
478     #[test]
adds_closing_brace_for_expr()479     fn adds_closing_brace_for_expr() {
480         type_char(
481             '{',
482             r#"
483 fn f() { match () { _ => $0() } }
484             "#,
485             r#"
486 fn f() { match () { _ => {()} } }
487             "#,
488         );
489         type_char(
490             '{',
491             r#"
492 fn f() { $0() }
493             "#,
494             r#"
495 fn f() { {()} }
496             "#,
497         );
498         type_char(
499             '{',
500             r#"
501 fn f() { let x = $0(); }
502             "#,
503             r#"
504 fn f() { let x = {()}; }
505             "#,
506         );
507         type_char(
508             '{',
509             r#"
510 fn f() { let x = $0a.b(); }
511             "#,
512             r#"
513 fn f() { let x = {a.b()}; }
514             "#,
515         );
516         type_char(
517             '{',
518             r#"
519 const S: () = $0();
520 fn f() {}
521             "#,
522             r#"
523 const S: () = {()};
524 fn f() {}
525             "#,
526         );
527         type_char(
528             '{',
529             r#"
530 const S: () = $0a.b();
531 fn f() {}
532             "#,
533             r#"
534 const S: () = {a.b()};
535 fn f() {}
536             "#,
537         );
538         type_char(
539             '{',
540             r#"
541 fn f() {
542     match x {
543         0 => $0(),
544         1 => (),
545     }
546 }
547             "#,
548             r#"
549 fn f() {
550     match x {
551         0 => {()},
552         1 => (),
553     }
554 }
555             "#,
556         );
557     }
558 
559     #[test]
noop_in_string_literal()560     fn noop_in_string_literal() {
561         // Regression test for #9351
562         type_char_noop(
563             '{',
564             r##"
565 fn check_with(ra_fixture: &str, expect: Expect) {
566     let base = r#"
567 enum E { T(), R$0, C }
568 use self::E::X;
569 const Z: E = E::C;
570 mod m {}
571 asdasdasdasdasdasda
572 sdasdasdasdasdasda
573 sdasdasdasdasd
574 "#;
575     let actual = completion_list(&format!("{}\n{}", base, ra_fixture));
576     expect.assert_eq(&actual)
577 }
578             "##,
579         );
580     }
581 
582     #[test]
adds_closing_brace_for_use_tree()583     fn adds_closing_brace_for_use_tree() {
584         type_char(
585             '{',
586             r#"
587 use some::$0Path;
588             "#,
589             r#"
590 use some::{Path};
591             "#,
592         );
593         type_char(
594             '{',
595             r#"
596 use some::{Path, $0Other};
597             "#,
598             r#"
599 use some::{Path, {Other}};
600             "#,
601         );
602         type_char(
603             '{',
604             r#"
605 use some::{$0Path, Other};
606             "#,
607             r#"
608 use some::{{Path}, Other};
609             "#,
610         );
611         type_char(
612             '{',
613             r#"
614 use some::path::$0to::Item;
615             "#,
616             r#"
617 use some::path::{to::Item};
618             "#,
619         );
620         type_char(
621             '{',
622             r#"
623 use some::$0path::to::Item;
624             "#,
625             r#"
626 use some::{path::to::Item};
627             "#,
628         );
629         type_char(
630             '{',
631             r#"
632 use $0some::path::to::Item;
633             "#,
634             r#"
635 use {some::path::to::Item};
636             "#,
637         );
638         type_char(
639             '{',
640             r#"
641 use some::path::$0to::{Item};
642             "#,
643             r#"
644 use some::path::{to::{Item}};
645             "#,
646         );
647         type_char(
648             '{',
649             r#"
650 use $0Thing as _;
651             "#,
652             r#"
653 use {Thing as _};
654             "#,
655         );
656 
657         type_char_noop(
658             '{',
659             r#"
660 use some::pa$0th::to::Item;
661             "#,
662         );
663     }
664 }
665