]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/typing.rs
Upgrade rowan
[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, mut before) = extract_offset(before);
149         let edit = TextEdit::insert(offset, char_typed.to_string());
150         edit.apply(&mut before);
151         let parse = SourceFile::parse(&before);
152         on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| {
153             it.apply(&mut before);
154             before.to_string()
155         })
156     }
157
158     fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
159         let actual = do_type_char(char_typed, ra_fixture_before)
160             .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
161
162         assert_eq_text!(ra_fixture_after, &actual);
163     }
164
165     fn type_char_noop(char_typed: char, before: &str) {
166         let file_change = do_type_char(char_typed, before);
167         assert!(file_change.is_none())
168     }
169
170     #[test]
171     fn test_on_eq_typed() {
172         //     do_check(r"
173         // fn foo() {
174         //     let foo =$0
175         // }
176         // ", r"
177         // fn foo() {
178         //     let foo =;
179         // }
180         // ");
181         type_char(
182             '=',
183             r"
184 fn foo() {
185     let foo $0 1 + 1
186 }
187 ",
188             r"
189 fn foo() {
190     let foo = 1 + 1;
191 }
192 ",
193         );
194         //     do_check(r"
195         // fn foo() {
196         //     let foo =$0
197         //     let bar = 1;
198         // }
199         // ", r"
200         // fn foo() {
201         //     let foo =;
202         //     let bar = 1;
203         // }
204         // ");
205     }
206
207     #[test]
208     fn indents_new_chain_call() {
209         type_char(
210             '.',
211             r"
212             fn main() {
213                 xs.foo()
214                 $0
215             }
216             ",
217             r"
218             fn main() {
219                 xs.foo()
220                     .
221             }
222             ",
223         );
224         type_char_noop(
225             '.',
226             r"
227             fn main() {
228                 xs.foo()
229                     $0
230             }
231             ",
232         )
233     }
234
235     #[test]
236     fn indents_new_chain_call_with_semi() {
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_let() {
265         type_char(
266             '.',
267             r#"
268 fn main() {
269     let _ = foo
270     $0
271     bar()
272 }
273 "#,
274             r#"
275 fn main() {
276     let _ = foo
277         .
278     bar()
279 }
280 "#,
281         );
282     }
283
284     #[test]
285     fn indents_continued_chain_call() {
286         type_char(
287             '.',
288             r"
289             fn main() {
290                 xs.foo()
291                     .first()
292                 $0
293             }
294             ",
295             r"
296             fn main() {
297                 xs.foo()
298                     .first()
299                     .
300             }
301             ",
302         );
303         type_char_noop(
304             '.',
305             r"
306             fn main() {
307                 xs.foo()
308                     .first()
309                     $0
310             }
311             ",
312         );
313     }
314
315     #[test]
316     fn indents_middle_of_chain_call() {
317         type_char(
318             '.',
319             r"
320             fn source_impl() {
321                 let var = enum_defvariant_list().unwrap()
322                 $0
323                     .nth(92)
324                     .unwrap();
325             }
326             ",
327             r"
328             fn source_impl() {
329                 let var = enum_defvariant_list().unwrap()
330                     .
331                     .nth(92)
332                     .unwrap();
333             }
334             ",
335         );
336         type_char_noop(
337             '.',
338             r"
339             fn source_impl() {
340                 let var = enum_defvariant_list().unwrap()
341                     $0
342                     .nth(92)
343                     .unwrap();
344             }
345             ",
346         );
347     }
348
349     #[test]
350     fn dont_indent_freestanding_dot() {
351         type_char_noop(
352             '.',
353             r"
354             fn main() {
355                 $0
356             }
357             ",
358         );
359         type_char_noop(
360             '.',
361             r"
362             fn main() {
363             $0
364             }
365             ",
366         );
367     }
368
369     #[test]
370     fn adds_space_after_return_type() {
371         type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
372     }
373 }