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