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