]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
Merge #8352
[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 pub(crate) const TRIGGER_CHARS: &str = ".=>";
37
38 // Feature: On Typing Assists
39 //
40 // Some features trigger on typing certain characters:
41 //
42 // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
43 // - typing `.` in a chain method call auto-indents
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     assert!(TRIGGER_CHARS.contains(char_typed));
61     let file = &db.parse(position.file_id).tree();
62     assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
63     let edit = on_char_typed_inner(file, position.offset, char_typed)?;
64     Some(SourceChange::from_text_edit(position.file_id, edit))
65 }
66
67 fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> {
68     assert!(TRIGGER_CHARS.contains(char_typed));
69     match char_typed {
70         '.' => on_dot_typed(file, offset),
71         '=' => on_eq_typed(file, offset),
72         '>' => on_arrow_typed(file, offset),
73         _ => unreachable!(),
74     }
75 }
76
77 /// Returns an edit which should be applied after `=` was typed. Primarily,
78 /// this works when adding `let =`.
79 // FIXME: use a snippet completion instead of this hack here.
80 fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
81     assert_eq!(file.syntax().text().char_at(offset), Some('='));
82     let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
83     if let_stmt.semicolon_token().is_some() {
84         return None;
85     }
86     if let Some(expr) = let_stmt.initializer() {
87         let expr_range = expr.syntax().text_range();
88         if expr_range.contains(offset) && offset != expr_range.start() {
89             return None;
90         }
91         if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
92             return None;
93         }
94     } else {
95         return None;
96     }
97     let offset = let_stmt.syntax().text_range().end();
98     Some(TextEdit::insert(offset, ";".to_string()))
99 }
100
101 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
102 fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
103     assert_eq!(file.syntax().text().char_at(offset), Some('.'));
104     let whitespace =
105         file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
106
107     let current_indent = {
108         let text = whitespace.text();
109         let newline = text.rfind('\n')?;
110         &text[newline + 1..]
111     };
112     let current_indent_len = TextSize::of(current_indent);
113
114     let parent = whitespace.syntax().parent()?;
115     // Make sure dot is a part of call chain
116     if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) {
117         return None;
118     }
119     let prev_indent = IndentLevel::from_node(&parent);
120     let target_indent = format!("    {}", prev_indent);
121     let target_indent_len = TextSize::of(&target_indent);
122     if current_indent_len == target_indent_len {
123         return None;
124     }
125
126     Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
127 }
128
129 /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
130 fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
131     let file_text = file.syntax().text();
132     assert_eq!(file_text.char_at(offset), Some('>'));
133     let after_arrow = offset + TextSize::of('>');
134     if file_text.char_at(after_arrow) != Some('{') {
135         return None;
136     }
137     if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
138         return None;
139     }
140
141     Some(TextEdit::insert(after_arrow, " ".to_string()))
142 }
143
144 #[cfg(test)]
145 mod tests {
146     use test_utils::{assert_eq_text, extract_offset};
147
148     use super::*;
149
150     fn do_type_char(char_typed: char, before: &str) -> Option<String> {
151         let (offset, mut before) = extract_offset(before);
152         let edit = TextEdit::insert(offset, char_typed.to_string());
153         edit.apply(&mut before);
154         let parse = SourceFile::parse(&before);
155         on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| {
156             it.apply(&mut before);
157             before.to_string()
158         })
159     }
160
161     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
162         let actual = do_type_char(char_typed, ra_fixture_before)
163             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
164
165         assert_eq_text!(ra_fixture_after, &actual);
166     }
167
168     fn type_char_noop(char_typed: char, before: &str) {
169         let file_change = do_type_char(char_typed, before);
170         assert!(file_change.is_none())
171     }
172
173     #[test]
174     fn test_on_eq_typed() {
175         //     do_check(r"
176         // fn foo() {
177         //     let foo =$0
178         // }
179         // ", r"
180         // fn foo() {
181         //     let foo =;
182         // }
183         // ");
184         type_char(
185             '=',
186             r"
187 fn foo() {
188     let foo $0 1 + 1
189 }
190 ",
191             r"
192 fn foo() {
193     let foo = 1 + 1;
194 }
195 ",
196         );
197         //     do_check(r"
198         // fn foo() {
199         //     let foo =$0
200         //     let bar = 1;
201         // }
202         // ", r"
203         // fn foo() {
204         //     let foo =;
205         //     let bar = 1;
206         // }
207         // ");
208     }
209
210     #[test]
211     fn indents_new_chain_call() {
212         type_char(
213             '.',
214             r"
215             fn main() {
216                 xs.foo()
217                 $0
218             }
219             ",
220             r"
221             fn main() {
222                 xs.foo()
223                     .
224             }
225             ",
226         );
227         type_char_noop(
228             '.',
229             r"
230             fn main() {
231                 xs.foo()
232                     $0
233             }
234             ",
235         )
236     }
237
238     #[test]
239     fn indents_new_chain_call_with_semi() {
240         type_char(
241             '.',
242             r"
243             fn main() {
244                 xs.foo()
245                 $0;
246             }
247             ",
248             r"
249             fn main() {
250                 xs.foo()
251                     .;
252             }
253             ",
254         );
255         type_char_noop(
256             '.',
257             r"
258             fn main() {
259                 xs.foo()
260                     $0;
261             }
262             ",
263         )
264     }
265
266     #[test]
267     fn indents_new_chain_call_with_let() {
268         type_char(
269             '.',
270             r#"
271 fn main() {
272     let _ = foo
273     $0
274     bar()
275 }
276 "#,
277             r#"
278 fn main() {
279     let _ = foo
280         .
281     bar()
282 }
283 "#,
284         );
285     }
286
287     #[test]
288     fn indents_continued_chain_call() {
289         type_char(
290             '.',
291             r"
292             fn main() {
293                 xs.foo()
294                     .first()
295                 $0
296             }
297             ",
298             r"
299             fn main() {
300                 xs.foo()
301                     .first()
302                     .
303             }
304             ",
305         );
306         type_char_noop(
307             '.',
308             r"
309             fn main() {
310                 xs.foo()
311                     .first()
312                     $0
313             }
314             ",
315         );
316     }
317
318     #[test]
319     fn indents_middle_of_chain_call() {
320         type_char(
321             '.',
322             r"
323             fn source_impl() {
324                 let var = enum_defvariant_list().unwrap()
325                 $0
326                     .nth(92)
327                     .unwrap();
328             }
329             ",
330             r"
331             fn source_impl() {
332                 let var = enum_defvariant_list().unwrap()
333                     .
334                     .nth(92)
335                     .unwrap();
336             }
337             ",
338         );
339         type_char_noop(
340             '.',
341             r"
342             fn source_impl() {
343                 let var = enum_defvariant_list().unwrap()
344                     $0
345                     .nth(92)
346                     .unwrap();
347             }
348             ",
349         );
350     }
351
352     #[test]
353     fn dont_indent_freestanding_dot() {
354         type_char_noop(
355             '.',
356             r"
357             fn main() {
358                 $0
359             }
360             ",
361         );
362         type_char_noop(
363             '.',
364             r"
365             fn main() {
366             $0
367             }
368             ",
369         );
370     }
371
372     #[test]
373     fn adds_space_after_return_type() {
374         type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
375     }
376 }