diff options
author | Igor Aleksanov <[email protected]> | 2020-08-14 05:34:07 +0100 |
---|---|---|
committer | Igor Aleksanov <[email protected]> | 2020-08-14 05:34:07 +0100 |
commit | c26c911ec1e6c2ad1dcb7d155a6a1d528839ad1a (patch) | |
tree | 7cff36c38234be0afb65273146d8247083a5cfeb /crates/ide/src/typing.rs | |
parent | 3c018bf84de5c693b5ee1c6bec0fed3b201c2060 (diff) | |
parent | f1f73649a686dc6e6449afc35e0fa6fed00e225d (diff) |
Merge branch 'master' into add-disable-diagnostics
Diffstat (limited to 'crates/ide/src/typing.rs')
-rw-r--r-- | crates/ide/src/typing.rs | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs new file mode 100644 index 000000000..899ce5f26 --- /dev/null +++ b/crates/ide/src/typing.rs | |||
@@ -0,0 +1,364 @@ | |||
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 | } | ||