]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
Merge #11579
[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, SyntaxKind, TextRange, TextSize,
26 };
27
28 use text_edit::{Indel, TextEdit};
29
30 use crate::SourceChange;
31
32 pub(crate) use on_enter::on_enter;
33
34 // Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`.
35 pub(crate) const TRIGGER_CHARS: &str = ".=>{";
36
37 // Feature: On Typing Assists
38 //
39 // Some features trigger on typing certain characters:
40 //
41 // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
42 // - typing `.` in a chain method call auto-indents
43 // - typing `{` in front of an expression inserts a closing `}` after the expression
44 //
45 // VS Code::
46 //
47 // Add the following to `settings.json`:
48 // [source,json]
49 // ----
50 // "editor.formatOnType": true,
51 // ----
52 //
53 // image::https://user-images.githubusercontent.com/48062697/113166163-69758500-923a-11eb-81ee-eb33ec380399.gif[]
54 // image::https://user-images.githubusercontent.com/48062697/113171066-105c2000-923f-11eb-87ab-f4a263346567.gif[]
55 pub(crate) fn on_char_typed(
56     db: &RootDatabase,
57     position: FilePosition,
58     char_typed: char,
59 ) -> Option<SourceChange> {
60     if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
61         return None;
62     }
63     let file = &db.parse(position.file_id);
64     if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) {
65         return None;
66     }
67     let edit = on_char_typed_inner(file, position.offset, char_typed)?;
68     Some(SourceChange::from_text_edit(position.file_id, edit))
69 }
70
71 fn on_char_typed_inner(
72     file: &Parse<SourceFile>,
73     offset: TextSize,
74     char_typed: char,
75 ) -> Option<TextEdit> {
76     if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
77         return None;
78     }
79     match char_typed {
80         '.' => on_dot_typed(&file.tree(), offset),
81         '=' => on_eq_typed(&file.tree(), offset),
82         '>' => on_arrow_typed(&file.tree(), offset),
83         '{' => on_opening_brace_typed(file, offset),
84         _ => unreachable!(),
85     }
86 }
87
88 /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
89 /// block, or a part of a `use` item.
90 fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> {
91     if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) {
92         return None;
93     }
94
95     let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?;
96     if brace_token.kind() != SyntaxKind::L_CURLY {
97         return None;
98     }
99
100     // Remove the `{` to get a better parse tree, and reparse.
101     let range = brace_token.text_range();
102     if !stdx::always!(range.len() == TextSize::of('{')) {
103         return None;
104     }
105     let file = file.reparse(&Indel::delete(range));
106
107     if let Some(edit) = brace_expr(&file.tree(), offset) {
108         return Some(edit);
109     }
110
111     if let Some(edit) = brace_use_path(&file.tree(), offset) {
112         return Some(edit);
113     }
114
115     return None;
116
117     fn brace_use_path(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
118         let segment: ast::PathSegment = find_node_at_offset(file.syntax(), offset)?;
119         if segment.syntax().text_range().start() != offset {
120             return None;
121         }
122
123         let tree: ast::UseTree = find_node_at_offset(file.syntax(), offset)?;
124
125         Some(TextEdit::insert(
126             tree.syntax().text_range().end() + TextSize::of("{"),
127             "}".to_string(),
128         ))
129     }
130
131     fn brace_expr(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
132         let mut expr: ast::Expr = find_node_at_offset(file.syntax(), offset)?;
133         if expr.syntax().text_range().start() != offset {
134             return None;
135         }
136
137         // Enclose the outermost expression starting at `offset`
138         while let Some(parent) = expr.syntax().parent() {
139             if parent.text_range().start() != expr.syntax().text_range().start() {
140                 break;
141             }
142
143             match ast::Expr::cast(parent) {
144                 Some(parent) => expr = parent,
145                 None => break,
146             }
147         }
148
149         // If it's a statement in a block, we don't know how many statements should be included
150         if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) {
151             return None;
152         }
153
154         // Insert `}` right after the expression.
155         Some(TextEdit::insert(
156             expr.syntax().text_range().end() + TextSize::of("{"),
157             "}".to_string(),
158         ))
159     }
160 }
161
162 /// Returns an edit which should be applied after `=` was typed. Primarily,
163 /// this works when adding `let =`.
164 // FIXME: use a snippet completion instead of this hack here.
165 fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
166     if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) {
167         return None;
168     }
169     let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
170     if let_stmt.semicolon_token().is_some() {
171         return None;
172     }
173     if let Some(expr) = let_stmt.initializer() {
174         let expr_range = expr.syntax().text_range();
175         if expr_range.contains(offset) && offset != expr_range.start() {
176             return None;
177         }
178         if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
179             return None;
180         }
181     } else {
182         return None;
183     }
184     let offset = let_stmt.syntax().text_range().end();
185     Some(TextEdit::insert(offset, ";".to_string()))
186 }
187
188 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
189 fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
190     if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) {
191         return None;
192     }
193     let whitespace =
194         file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
195
196     // if prior is fn call over multiple lines dont indent
197     // or if previous is method call over multiples lines keep that indent
198     let current_indent = {
199         let text = whitespace.text();
200         let (_prefix, suffix) = text.rsplit_once('\n')?;
201         suffix
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     let receiver = if let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) {
208         field_expr.expr()?
209     } else if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent.clone()) {
210         method_call_expr.receiver()?
211     } else {
212         return None;
213     };
214
215     let receiver_is_multiline = receiver.syntax().text().find_char('\n').is_some();
216     let target_indent = match (receiver, receiver_is_multiline) {
217         // if receiver is multiline field or method call, just take the previous `.` indentation
218         (ast::Expr::MethodCallExpr(expr), true) => {
219             expr.dot_token().as_ref().map(IndentLevel::from_token)
220         }
221         (ast::Expr::FieldExpr(expr), true) => {
222             expr.dot_token().as_ref().map(IndentLevel::from_token)
223         }
224         // if receiver is multiline expression, just keeps its indentation
225         (_, true) => Some(IndentLevel::from_node(&parent)),
226         _ => None,
227     };
228     let target_indent = match target_indent {
229         Some(x) => x,
230         // in all other cases, take previous indentation and indent once
231         None => IndentLevel::from_node(&parent) + 1,
232     }
233     .to_string();
234
235     if current_indent_len == TextSize::of(&target_indent) {
236         return None;
237     }
238
239     Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
240 }
241
242 /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
243 fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
244     let file_text = file.syntax().text();
245     if !stdx::always!(file_text.char_at(offset) == Some('>')) {
246         return None;
247     }
248     let after_arrow = offset + TextSize::of('>');
249     if file_text.char_at(after_arrow) != Some('{') {
250         return None;
251     }
252     if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
253         return None;
254     }
255
256     Some(TextEdit::insert(after_arrow, " ".to_string()))
257 }
258
259 #[cfg(test)]
260 mod tests {
261     use test_utils::{assert_eq_text, extract_offset};
262
263     use super::*;
264
265     fn do_type_char(char_typed: char, before: &str) -> Option<String> {
266         let (offset, mut before) = extract_offset(before);
267         let edit = TextEdit::insert(offset, char_typed.to_string());
268         edit.apply(&mut before);
269         let parse = SourceFile::parse(&before);
270         on_char_typed_inner(&parse, offset, char_typed).map(|it| {
271             it.apply(&mut before);
272             before.to_string()
273         })
274     }
275
276     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
277         let actual = do_type_char(char_typed, ra_fixture_before)
278             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
279
280         assert_eq_text!(ra_fixture_after, &actual);
281     }
282
283     fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
284         let file_change = do_type_char(char_typed, ra_fixture_before);
285         assert!(file_change.is_none())
286     }
287
288     #[test]
289     fn test_on_eq_typed() {
290         //     do_check(r"
291         // fn foo() {
292         //     let foo =$0
293         // }
294         // ", r"
295         // fn foo() {
296         //     let foo =;
297         // }
298         // ");
299         type_char(
300             '=',
301             r#"
302 fn foo() {
303     let foo $0 1 + 1
304 }
305 "#,
306             r#"
307 fn foo() {
308     let foo = 1 + 1;
309 }
310 "#,
311         );
312         //     do_check(r"
313         // fn foo() {
314         //     let foo =$0
315         //     let bar = 1;
316         // }
317         // ", r"
318         // fn foo() {
319         //     let foo =;
320         //     let bar = 1;
321         // }
322         // ");
323     }
324
325     #[test]
326     fn indents_new_chain_call() {
327         type_char(
328             '.',
329             r#"
330 fn main() {
331     xs.foo()
332     $0
333 }
334             "#,
335             r#"
336 fn main() {
337     xs.foo()
338         .
339 }
340             "#,
341         );
342         type_char_noop(
343             '.',
344             r#"
345 fn main() {
346     xs.foo()
347         $0
348 }
349             "#,
350         )
351     }
352
353     #[test]
354     fn indents_new_chain_call_with_semi() {
355         type_char(
356             '.',
357             r"
358 fn main() {
359     xs.foo()
360     $0;
361 }
362             ",
363             r#"
364 fn main() {
365     xs.foo()
366         .;
367 }
368             "#,
369         );
370         type_char_noop(
371             '.',
372             r#"
373 fn main() {
374     xs.foo()
375         $0;
376 }
377             "#,
378         )
379     }
380
381     #[test]
382     fn indents_new_chain_call_with_let() {
383         type_char(
384             '.',
385             r#"
386 fn main() {
387     let _ = foo
388     $0
389     bar()
390 }
391 "#,
392             r#"
393 fn main() {
394     let _ = foo
395         .
396     bar()
397 }
398 "#,
399         );
400     }
401
402     #[test]
403     fn indents_continued_chain_call() {
404         type_char(
405             '.',
406             r#"
407 fn main() {
408     xs.foo()
409         .first()
410     $0
411 }
412             "#,
413             r#"
414 fn main() {
415     xs.foo()
416         .first()
417         .
418 }
419             "#,
420         );
421         type_char_noop(
422             '.',
423             r#"
424 fn main() {
425     xs.foo()
426         .first()
427         $0
428 }
429             "#,
430         );
431     }
432
433     #[test]
434     fn indents_middle_of_chain_call() {
435         type_char(
436             '.',
437             r#"
438 fn source_impl() {
439     let var = enum_defvariant_list().unwrap()
440     $0
441         .nth(92)
442         .unwrap();
443 }
444             "#,
445             r#"
446 fn source_impl() {
447     let var = enum_defvariant_list().unwrap()
448         .
449         .nth(92)
450         .unwrap();
451 }
452             "#,
453         );
454         type_char_noop(
455             '.',
456             r#"
457 fn source_impl() {
458     let var = enum_defvariant_list().unwrap()
459         $0
460         .nth(92)
461         .unwrap();
462 }
463             "#,
464         );
465     }
466
467     #[test]
468     fn dont_indent_freestanding_dot() {
469         type_char_noop(
470             '.',
471             r#"
472 fn main() {
473     $0
474 }
475             "#,
476         );
477         type_char_noop(
478             '.',
479             r#"
480 fn main() {
481 $0
482 }
483             "#,
484         );
485     }
486
487     #[test]
488     fn adds_space_after_return_type() {
489         type_char(
490             '>',
491             r#"
492 fn foo() -$0{ 92 }
493 "#,
494             r#"
495 fn foo() -> { 92 }
496 "#,
497         );
498     }
499
500     #[test]
501     fn adds_closing_brace_for_expr() {
502         type_char(
503             '{',
504             r#"
505 fn f() { match () { _ => $0() } }
506             "#,
507             r#"
508 fn f() { match () { _ => {()} } }
509             "#,
510         );
511         type_char(
512             '{',
513             r#"
514 fn f() { $0() }
515             "#,
516             r#"
517 fn f() { {()} }
518             "#,
519         );
520         type_char(
521             '{',
522             r#"
523 fn f() { let x = $0(); }
524             "#,
525             r#"
526 fn f() { let x = {()}; }
527             "#,
528         );
529         type_char(
530             '{',
531             r#"
532 fn f() { let x = $0a.b(); }
533             "#,
534             r#"
535 fn f() { let x = {a.b()}; }
536             "#,
537         );
538         type_char(
539             '{',
540             r#"
541 const S: () = $0();
542 fn f() {}
543             "#,
544             r#"
545 const S: () = {()};
546 fn f() {}
547             "#,
548         );
549         type_char(
550             '{',
551             r#"
552 const S: () = $0a.b();
553 fn f() {}
554             "#,
555             r#"
556 const S: () = {a.b()};
557 fn f() {}
558             "#,
559         );
560         type_char(
561             '{',
562             r#"
563 fn f() {
564     match x {
565         0 => $0(),
566         1 => (),
567     }
568 }
569             "#,
570             r#"
571 fn f() {
572     match x {
573         0 => {()},
574         1 => (),
575     }
576 }
577             "#,
578         );
579     }
580
581     #[test]
582     fn noop_in_string_literal() {
583         // Regression test for #9351
584         type_char_noop(
585             '{',
586             r##"
587 fn check_with(ra_fixture: &str, expect: Expect) {
588     let base = r#"
589 enum E { T(), R$0, C }
590 use self::E::X;
591 const Z: E = E::C;
592 mod m {}
593 asdasdasdasdasdasda
594 sdasdasdasdasdasda
595 sdasdasdasdasd
596 "#;
597     let actual = completion_list(&format!("{}\n{}", base, ra_fixture));
598     expect.assert_eq(&actual)
599 }
600             "##,
601         );
602     }
603
604     #[test]
605     fn adds_closing_brace_for_use_tree() {
606         type_char(
607             '{',
608             r#"
609 use some::$0Path;
610             "#,
611             r#"
612 use some::{Path};
613             "#,
614         );
615         type_char(
616             '{',
617             r#"
618 use some::{Path, $0Other};
619             "#,
620             r#"
621 use some::{Path, {Other}};
622             "#,
623         );
624         type_char(
625             '{',
626             r#"
627 use some::{$0Path, Other};
628             "#,
629             r#"
630 use some::{{Path}, Other};
631             "#,
632         );
633         type_char(
634             '{',
635             r#"
636 use some::path::$0to::Item;
637             "#,
638             r#"
639 use some::path::{to::Item};
640             "#,
641         );
642         type_char(
643             '{',
644             r#"
645 use some::$0path::to::Item;
646             "#,
647             r#"
648 use some::{path::to::Item};
649             "#,
650         );
651         type_char(
652             '{',
653             r#"
654 use $0some::path::to::Item;
655             "#,
656             r#"
657 use {some::path::to::Item};
658             "#,
659         );
660         type_char(
661             '{',
662             r#"
663 use some::path::$0to::{Item};
664             "#,
665             r#"
666 use some::path::{to::{Item}};
667             "#,
668         );
669         type_char(
670             '{',
671             r#"
672 use $0Thing as _;
673             "#,
674             r#"
675 use {Thing as _};
676             "#,
677         );
678
679         type_char_noop(
680             '{',
681             r#"
682 use some::pa$0th::to::Item;
683             "#,
684         );
685     }
686
687     #[test]
688     fn regression_629() {
689         type_char_noop(
690             '.',
691             r#"
692 fn foo() {
693     CompletionItem::new(
694         CompletionKind::Reference,
695         ctx.source_range(),
696         field.name().to_string(),
697     )
698     .foo()
699     $0
700 }
701 "#,
702         );
703         type_char_noop(
704             '.',
705             r#"
706 fn foo() {
707     CompletionItem::new(
708         CompletionKind::Reference,
709         ctx.source_range(),
710         field.name().to_string(),
711     )
712     $0
713 }
714 "#,
715         );
716     }
717 }