]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
Merge #8482
[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, ra_fixture_before: &str) {
226         let file_change = do_type_char(char_typed, ra_fixture_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(
432             '>',
433             r#"
434 fn foo() -$0{ 92 }
435 "#,
436             r#"
437 fn foo() -> { 92 }
438 "#,
439         );
440     }
441
442     #[test]
443     fn adds_closing_brace() {
444         type_char(
445             '{',
446             r#"
447 fn f() { match () { _ => $0() } }
448             "#,
449             r#"
450 fn f() { match () { _ => {()} } }
451             "#,
452         );
453         type_char(
454             '{',
455             r#"
456 fn f() { $0() }
457             "#,
458             r#"
459 fn f() { {()} }
460             "#,
461         );
462         type_char(
463             '{',
464             r#"
465 fn f() { let x = $0(); }
466             "#,
467             r#"
468 fn f() { let x = {()}; }
469             "#,
470         );
471         type_char(
472             '{',
473             r#"
474 fn f() { let x = $0a.b(); }
475             "#,
476             r#"
477 fn f() { let x = {a.b()}; }
478             "#,
479         );
480         type_char(
481             '{',
482             r#"
483 const S: () = $0();
484 fn f() {}
485             "#,
486             r#"
487 const S: () = {()};
488 fn f() {}
489             "#,
490         );
491         type_char(
492             '{',
493             r#"
494 const S: () = $0a.b();
495 fn f() {}
496             "#,
497             r#"
498 const S: () = {a.b()};
499 fn f() {}
500             "#,
501         );
502         type_char(
503             '{',
504             r#"
505 fn f() {
506     match x {
507         0 => $0(),
508         1 => (),
509     }
510 }
511             "#,
512             r#"
513 fn f() {
514     match x {
515         0 => {()},
516         1 => (),
517     }
518 }
519             "#,
520         );
521     }
522 }