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