]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
more clippy fixes
[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::{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[]
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, or a part of a `use` item.
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.
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.
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() -> { ... }`
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     find_node_at_offset::<ast::RetType>(file.syntax(), offset)?;
231
232     Some(TextEdit::insert(after_arrow, " ".to_string()))
233 }
234
235 #[cfg(test)]
236 mod tests {
237     use test_utils::{assert_eq_text, extract_offset};
238
239     use super::*;
240
241     fn do_type_char(char_typed: char, before: &str) -> Option<String> {
242         let (offset, mut before) = extract_offset(before);
243         let edit = TextEdit::insert(offset, char_typed.to_string());
244         edit.apply(&mut before);
245         let parse = SourceFile::parse(&before);
246         on_char_typed_inner(&parse, offset, char_typed).map(|it| {
247             it.apply(&mut before);
248             before.to_string()
249         })
250     }
251
252     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
253         let actual = do_type_char(char_typed, ra_fixture_before)
254             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
255
256         assert_eq_text!(ra_fixture_after, &actual);
257     }
258
259     fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
260         let file_change = do_type_char(char_typed, ra_fixture_before);
261         assert!(file_change.is_none())
262     }
263
264     #[test]
265     fn test_on_eq_typed() {
266         //     do_check(r"
267         // fn foo() {
268         //     let foo =$0
269         // }
270         // ", r"
271         // fn foo() {
272         //     let foo =;
273         // }
274         // ");
275         type_char(
276             '=',
277             r#"
278 fn foo() {
279     let foo $0 1 + 1
280 }
281 "#,
282             r#"
283 fn foo() {
284     let foo = 1 + 1;
285 }
286 "#,
287         );
288         //     do_check(r"
289         // fn foo() {
290         //     let foo =$0
291         //     let bar = 1;
292         // }
293         // ", r"
294         // fn foo() {
295         //     let foo =;
296         //     let bar = 1;
297         // }
298         // ");
299     }
300
301     #[test]
302     fn indents_new_chain_call() {
303         type_char(
304             '.',
305             r#"
306 fn main() {
307     xs.foo()
308     $0
309 }
310             "#,
311             r#"
312 fn main() {
313     xs.foo()
314         .
315 }
316             "#,
317         );
318         type_char_noop(
319             '.',
320             r#"
321 fn main() {
322     xs.foo()
323         $0
324 }
325             "#,
326         )
327     }
328
329     #[test]
330     fn indents_new_chain_call_with_semi() {
331         type_char(
332             '.',
333             r"
334 fn main() {
335     xs.foo()
336     $0;
337 }
338             ",
339             r#"
340 fn main() {
341     xs.foo()
342         .;
343 }
344             "#,
345         );
346         type_char_noop(
347             '.',
348             r#"
349 fn main() {
350     xs.foo()
351         $0;
352 }
353             "#,
354         )
355     }
356
357     #[test]
358     fn indents_new_chain_call_with_let() {
359         type_char(
360             '.',
361             r#"
362 fn main() {
363     let _ = foo
364     $0
365     bar()
366 }
367 "#,
368             r#"
369 fn main() {
370     let _ = foo
371         .
372     bar()
373 }
374 "#,
375         );
376     }
377
378     #[test]
379     fn indents_continued_chain_call() {
380         type_char(
381             '.',
382             r#"
383 fn main() {
384     xs.foo()
385         .first()
386     $0
387 }
388             "#,
389             r#"
390 fn main() {
391     xs.foo()
392         .first()
393         .
394 }
395             "#,
396         );
397         type_char_noop(
398             '.',
399             r#"
400 fn main() {
401     xs.foo()
402         .first()
403         $0
404 }
405             "#,
406         );
407     }
408
409     #[test]
410     fn indents_middle_of_chain_call() {
411         type_char(
412             '.',
413             r#"
414 fn source_impl() {
415     let var = enum_defvariant_list().unwrap()
416     $0
417         .nth(92)
418         .unwrap();
419 }
420             "#,
421             r#"
422 fn source_impl() {
423     let var = enum_defvariant_list().unwrap()
424         .
425         .nth(92)
426         .unwrap();
427 }
428             "#,
429         );
430         type_char_noop(
431             '.',
432             r#"
433 fn source_impl() {
434     let var = enum_defvariant_list().unwrap()
435         $0
436         .nth(92)
437         .unwrap();
438 }
439             "#,
440         );
441     }
442
443     #[test]
444     fn dont_indent_freestanding_dot() {
445         type_char_noop(
446             '.',
447             r#"
448 fn main() {
449     $0
450 }
451             "#,
452         );
453         type_char_noop(
454             '.',
455             r#"
456 fn main() {
457 $0
458 }
459             "#,
460         );
461     }
462
463     #[test]
464     fn adds_space_after_return_type() {
465         type_char(
466             '>',
467             r#"
468 fn foo() -$0{ 92 }
469 "#,
470             r#"
471 fn foo() -> { 92 }
472 "#,
473         );
474     }
475
476     #[test]
477     fn adds_closing_brace_for_expr() {
478         type_char(
479             '{',
480             r#"
481 fn f() { match () { _ => $0() } }
482             "#,
483             r#"
484 fn f() { match () { _ => {()} } }
485             "#,
486         );
487         type_char(
488             '{',
489             r#"
490 fn f() { $0() }
491             "#,
492             r#"
493 fn f() { {()} }
494             "#,
495         );
496         type_char(
497             '{',
498             r#"
499 fn f() { let x = $0(); }
500             "#,
501             r#"
502 fn f() { let x = {()}; }
503             "#,
504         );
505         type_char(
506             '{',
507             r#"
508 fn f() { let x = $0a.b(); }
509             "#,
510             r#"
511 fn f() { let x = {a.b()}; }
512             "#,
513         );
514         type_char(
515             '{',
516             r#"
517 const S: () = $0();
518 fn f() {}
519             "#,
520             r#"
521 const S: () = {()};
522 fn f() {}
523             "#,
524         );
525         type_char(
526             '{',
527             r#"
528 const S: () = $0a.b();
529 fn f() {}
530             "#,
531             r#"
532 const S: () = {a.b()};
533 fn f() {}
534             "#,
535         );
536         type_char(
537             '{',
538             r#"
539 fn f() {
540     match x {
541         0 => $0(),
542         1 => (),
543     }
544 }
545             "#,
546             r#"
547 fn f() {
548     match x {
549         0 => {()},
550         1 => (),
551     }
552 }
553             "#,
554         );
555     }
556
557     #[test]
558     fn noop_in_string_literal() {
559         // Regression test for #9351
560         type_char_noop(
561             '{',
562             r##"
563 fn check_with(ra_fixture: &str, expect: Expect) {
564     let base = r#"
565 enum E { T(), R$0, C }
566 use self::E::X;
567 const Z: E = E::C;
568 mod m {}
569 asdasdasdasdasdasda
570 sdasdasdasdasdasda
571 sdasdasdasdasd
572 "#;
573     let actual = completion_list(&format!("{}\n{}", base, ra_fixture));
574     expect.assert_eq(&actual)
575 }
576             "##,
577         );
578     }
579
580     #[test]
581     fn adds_closing_brace_for_use_tree() {
582         type_char(
583             '{',
584             r#"
585 use some::$0Path;
586             "#,
587             r#"
588 use some::{Path};
589             "#,
590         );
591         type_char(
592             '{',
593             r#"
594 use some::{Path, $0Other};
595             "#,
596             r#"
597 use some::{Path, {Other}};
598             "#,
599         );
600         type_char(
601             '{',
602             r#"
603 use some::{$0Path, Other};
604             "#,
605             r#"
606 use some::{{Path}, Other};
607             "#,
608         );
609         type_char(
610             '{',
611             r#"
612 use some::path::$0to::Item;
613             "#,
614             r#"
615 use some::path::{to::Item};
616             "#,
617         );
618         type_char(
619             '{',
620             r#"
621 use some::$0path::to::Item;
622             "#,
623             r#"
624 use some::{path::to::Item};
625             "#,
626         );
627         type_char(
628             '{',
629             r#"
630 use $0some::path::to::Item;
631             "#,
632             r#"
633 use {some::path::to::Item};
634             "#,
635         );
636         type_char(
637             '{',
638             r#"
639 use some::path::$0to::{Item};
640             "#,
641             r#"
642 use some::path::{to::{Item}};
643             "#,
644         );
645         type_char(
646             '{',
647             r#"
648 use $0Thing as _;
649             "#,
650             r#"
651 use {Thing as _};
652             "#,
653         );
654
655         type_char_noop(
656             '{',
657             r#"
658 use some::pa$0th::to::Item;
659             "#,
660         );
661     }
662 }