]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
Use stdx::always
[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, SourceFile,
26     SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR},
27     TextRange, TextSize,
28 };
29
30 use text_edit::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 //
46 // VS Code::
47 //
48 // Add the following to `settings.json`:
49 // [source,json]
50 // ----
51 // "editor.formatOnType": true,
52 // ----
53 //
54 // image::https://user-images.githubusercontent.com/48062697/113166163-69758500-923a-11eb-81ee-eb33ec380399.gif[]
55 // image::https://user-images.githubusercontent.com/48062697/113171066-105c2000-923f-11eb-87ab-f4a263346567.gif[]
56 pub(crate) fn on_char_typed(
57     db: &RootDatabase,
58     position: FilePosition,
59     char_typed: char,
60 ) -> Option<SourceChange> {
61     assert!(TRIGGER_CHARS.contains(char_typed));
62     let file = &db.parse(position.file_id).tree();
63     assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
64     let edit = on_char_typed_inner(file, position.offset, char_typed)?;
65     Some(SourceChange::from_text_edit(position.file_id, edit))
66 }
67
68 fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> {
69     assert!(TRIGGER_CHARS.contains(char_typed));
70     match char_typed {
71         '.' => on_dot_typed(file, offset),
72         '=' => on_eq_typed(file, offset),
73         '>' => on_arrow_typed(file, offset),
74         '{' => on_opening_brace_typed(file, offset),
75         _ => unreachable!(),
76     }
77 }
78
79 /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
80 /// block.
81 fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
82     stdx::always!(file.syntax().text().char_at(offset) == Some('{'));
83     let brace_token = file.syntax().token_at_offset(offset).right_biased()?;
84     let block = ast::BlockExpr::cast(brace_token.parent()?)?;
85
86     // We expect a block expression enclosing exactly 1 preexisting expression. It can be parsed as
87     // either the trailing expr or an ExprStmt.
88     let offset = {
89         match block.statements().next() {
90             Some(ast::Stmt::ExprStmt(it)) => {
91                 // Use the expression span to place `}` before the `;`
92                 it.expr()?.syntax().text_range().end()
93             }
94             None => block.tail_expr()?.syntax().text_range().end(),
95             _ => return None,
96         }
97     };
98
99     Some(TextEdit::insert(offset, "}".to_string()))
100 }
101
102 /// Returns an edit which should be applied after `=` was typed. Primarily,
103 /// this works when adding `let =`.
104 // FIXME: use a snippet completion instead of this hack here.
105 fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
106     stdx::always!(file.syntax().text().char_at(offset) == Some('='));
107     let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
108     if let_stmt.semicolon_token().is_some() {
109         return None;
110     }
111     if let Some(expr) = let_stmt.initializer() {
112         let expr_range = expr.syntax().text_range();
113         if expr_range.contains(offset) && offset != expr_range.start() {
114             return None;
115         }
116         if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
117             return None;
118         }
119     } else {
120         return None;
121     }
122     let offset = let_stmt.syntax().text_range().end();
123     Some(TextEdit::insert(offset, ";".to_string()))
124 }
125
126 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
127 fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
128     stdx::always!(file.syntax().text().char_at(offset) == Some('.'));
129     let whitespace =
130         file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
131
132     let current_indent = {
133         let text = whitespace.text();
134         let newline = text.rfind('\n')?;
135         &text[newline + 1..]
136     };
137     let current_indent_len = TextSize::of(current_indent);
138
139     let parent = whitespace.syntax().parent()?;
140     // Make sure dot is a part of call chain
141     if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) {
142         return None;
143     }
144     let prev_indent = IndentLevel::from_node(&parent);
145     let target_indent = format!("    {}", prev_indent);
146     let target_indent_len = TextSize::of(&target_indent);
147     if current_indent_len == target_indent_len {
148         return None;
149     }
150
151     Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
152 }
153
154 /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
155 fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
156     let file_text = file.syntax().text();
157     stdx::always!(file_text.char_at(offset) == Some('>'));
158     let after_arrow = offset + TextSize::of('>');
159     if file_text.char_at(after_arrow) != Some('{') {
160         return None;
161     }
162     if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
163         return None;
164     }
165
166     Some(TextEdit::insert(after_arrow, " ".to_string()))
167 }
168
169 #[cfg(test)]
170 mod tests {
171     use test_utils::{assert_eq_text, extract_offset};
172
173     use super::*;
174
175     fn do_type_char(char_typed: char, before: &str) -> Option<String> {
176         let (offset, mut before) = extract_offset(before);
177         let edit = TextEdit::insert(offset, char_typed.to_string());
178         edit.apply(&mut before);
179         let parse = SourceFile::parse(&before);
180         on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| {
181             it.apply(&mut before);
182             before.to_string()
183         })
184     }
185
186     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
187         let actual = do_type_char(char_typed, ra_fixture_before)
188             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
189
190         assert_eq_text!(ra_fixture_after, &actual);
191     }
192
193     fn type_char_noop(char_typed: char, before: &str) {
194         let file_change = do_type_char(char_typed, before);
195         assert!(file_change.is_none())
196     }
197
198     #[test]
199     fn test_on_eq_typed() {
200         //     do_check(r"
201         // fn foo() {
202         //     let foo =$0
203         // }
204         // ", r"
205         // fn foo() {
206         //     let foo =;
207         // }
208         // ");
209         type_char(
210             '=',
211             r"
212 fn foo() {
213     let foo $0 1 + 1
214 }
215 ",
216             r"
217 fn foo() {
218     let foo = 1 + 1;
219 }
220 ",
221         );
222         //     do_check(r"
223         // fn foo() {
224         //     let foo =$0
225         //     let bar = 1;
226         // }
227         // ", r"
228         // fn foo() {
229         //     let foo =;
230         //     let bar = 1;
231         // }
232         // ");
233     }
234
235     #[test]
236     fn indents_new_chain_call() {
237         type_char(
238             '.',
239             r"
240             fn main() {
241                 xs.foo()
242                 $0
243             }
244             ",
245             r"
246             fn main() {
247                 xs.foo()
248                     .
249             }
250             ",
251         );
252         type_char_noop(
253             '.',
254             r"
255             fn main() {
256                 xs.foo()
257                     $0
258             }
259             ",
260         )
261     }
262
263     #[test]
264     fn indents_new_chain_call_with_semi() {
265         type_char(
266             '.',
267             r"
268             fn main() {
269                 xs.foo()
270                 $0;
271             }
272             ",
273             r"
274             fn main() {
275                 xs.foo()
276                     .;
277             }
278             ",
279         );
280         type_char_noop(
281             '.',
282             r"
283             fn main() {
284                 xs.foo()
285                     $0;
286             }
287             ",
288         )
289     }
290
291     #[test]
292     fn indents_new_chain_call_with_let() {
293         type_char(
294             '.',
295             r#"
296 fn main() {
297     let _ = foo
298     $0
299     bar()
300 }
301 "#,
302             r#"
303 fn main() {
304     let _ = foo
305         .
306     bar()
307 }
308 "#,
309         );
310     }
311
312     #[test]
313     fn indents_continued_chain_call() {
314         type_char(
315             '.',
316             r"
317             fn main() {
318                 xs.foo()
319                     .first()
320                 $0
321             }
322             ",
323             r"
324             fn main() {
325                 xs.foo()
326                     .first()
327                     .
328             }
329             ",
330         );
331         type_char_noop(
332             '.',
333             r"
334             fn main() {
335                 xs.foo()
336                     .first()
337                     $0
338             }
339             ",
340         );
341     }
342
343     #[test]
344     fn indents_middle_of_chain_call() {
345         type_char(
346             '.',
347             r"
348             fn source_impl() {
349                 let var = enum_defvariant_list().unwrap()
350                 $0
351                     .nth(92)
352                     .unwrap();
353             }
354             ",
355             r"
356             fn source_impl() {
357                 let var = enum_defvariant_list().unwrap()
358                     .
359                     .nth(92)
360                     .unwrap();
361             }
362             ",
363         );
364         type_char_noop(
365             '.',
366             r"
367             fn source_impl() {
368                 let var = enum_defvariant_list().unwrap()
369                     $0
370                     .nth(92)
371                     .unwrap();
372             }
373             ",
374         );
375     }
376
377     #[test]
378     fn dont_indent_freestanding_dot() {
379         type_char_noop(
380             '.',
381             r"
382             fn main() {
383                 $0
384             }
385             ",
386         );
387         type_char_noop(
388             '.',
389             r"
390             fn main() {
391             $0
392             }
393             ",
394         );
395     }
396
397     #[test]
398     fn adds_space_after_return_type() {
399         type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
400     }
401
402     #[test]
403     fn adds_closing_brace() {
404         type_char('{', r"fn f() { match () { _ => $0() } }", r"fn f() { match () { _ => {()} } }");
405         type_char('{', r"fn f() { $0(); }", r"fn f() { {()}; }");
406         type_char('{', r"fn f() { let x = $0(); }", r"fn f() { let x = {()}; }");
407         type_char(
408             '{',
409             r"
410             const S: () = $0();
411             fn f() {}
412             ",
413             r"
414             const S: () = {()};
415             fn f() {}
416             ",
417         );
418         type_char(
419             '{',
420             r"
421             fn f() {
422                 match x {
423                     0 => $0(),
424                     1 => (),
425                 }
426             }",
427             r"
428             fn f() {
429                 match x {
430                     0 => {()},
431                     1 => (),
432                 }
433             }",
434         );
435     }
436 }