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