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