diff options
Diffstat (limited to 'crates/ide/src/typing.rs')
-rw-r--r-- | crates/ide/src/typing.rs | 371 |
1 files changed, 260 insertions, 111 deletions
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index e10b7d98e..82c732390 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs | |||
@@ -22,18 +22,19 @@ use ide_db::{ | |||
22 | use syntax::{ | 22 | use syntax::{ |
23 | algo::find_node_at_offset, | 23 | algo::find_node_at_offset, |
24 | ast::{self, edit::IndentLevel, AstToken}, | 24 | ast::{self, edit::IndentLevel, AstToken}, |
25 | AstNode, SourceFile, | 25 | AstNode, Parse, SourceFile, |
26 | SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, | 26 | SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, |
27 | TextRange, TextSize, | 27 | TextRange, TextSize, |
28 | }; | 28 | }; |
29 | 29 | ||
30 | use text_edit::TextEdit; | 30 | use text_edit::{Indel, TextEdit}; |
31 | 31 | ||
32 | use crate::SourceChange; | 32 | use crate::SourceChange; |
33 | 33 | ||
34 | pub(crate) use on_enter::on_enter; | 34 | pub(crate) use on_enter::on_enter; |
35 | 35 | ||
36 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; | 36 | // Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`. |
37 | pub(crate) const TRIGGER_CHARS: &str = ".=>{"; | ||
37 | 38 | ||
38 | // Feature: On Typing Assists | 39 | // Feature: On Typing Assists |
39 | // | 40 | // |
@@ -41,6 +42,7 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>"; | |||
41 | // | 42 | // |
42 | // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression | 43 | // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression |
43 | // - typing `.` in a chain method call auto-indents | 44 | // - typing `.` in a chain method call auto-indents |
45 | // - typing `{` in front of an expression inserts a closing `}` after the expression | ||
44 | // | 46 | // |
45 | // VS Code:: | 47 | // VS Code:: |
46 | // | 48 | // |
@@ -49,33 +51,87 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>"; | |||
49 | // ---- | 51 | // ---- |
50 | // "editor.formatOnType": true, | 52 | // "editor.formatOnType": true, |
51 | // ---- | 53 | // ---- |
54 | // | ||
55 | // image::https://user-images.githubusercontent.com/48062697/113166163-69758500-923a-11eb-81ee-eb33ec380399.gif[] | ||
56 | // image::https://user-images.githubusercontent.com/48062697/113171066-105c2000-923f-11eb-87ab-f4a263346567.gif[] | ||
52 | pub(crate) fn on_char_typed( | 57 | pub(crate) fn on_char_typed( |
53 | db: &RootDatabase, | 58 | db: &RootDatabase, |
54 | position: FilePosition, | 59 | position: FilePosition, |
55 | char_typed: char, | 60 | char_typed: char, |
56 | ) -> Option<SourceChange> { | 61 | ) -> Option<SourceChange> { |
57 | assert!(TRIGGER_CHARS.contains(char_typed)); | 62 | if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { |
58 | let file = &db.parse(position.file_id).tree(); | 63 | return None; |
59 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | 64 | } |
65 | let file = &db.parse(position.file_id); | ||
66 | if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) { | ||
67 | return None; | ||
68 | } | ||
60 | let edit = on_char_typed_inner(file, position.offset, char_typed)?; | 69 | let edit = on_char_typed_inner(file, position.offset, char_typed)?; |
61 | Some(SourceChange::from_text_edit(position.file_id, edit)) | 70 | Some(SourceChange::from_text_edit(position.file_id, edit)) |
62 | } | 71 | } |
63 | 72 | ||
64 | fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { | 73 | fn on_char_typed_inner( |
65 | assert!(TRIGGER_CHARS.contains(char_typed)); | 74 | file: &Parse<SourceFile>, |
75 | offset: TextSize, | ||
76 | char_typed: char, | ||
77 | ) -> Option<TextEdit> { | ||
78 | if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { | ||
79 | return None; | ||
80 | } | ||
66 | match char_typed { | 81 | match char_typed { |
67 | '.' => on_dot_typed(file, offset), | 82 | '.' => on_dot_typed(&file.tree(), offset), |
68 | '=' => on_eq_typed(file, offset), | 83 | '=' => on_eq_typed(&file.tree(), offset), |
69 | '>' => on_arrow_typed(file, offset), | 84 | '>' => on_arrow_typed(&file.tree(), offset), |
85 | '{' => on_opening_brace_typed(file, offset), | ||
70 | _ => unreachable!(), | 86 | _ => unreachable!(), |
71 | } | 87 | } |
72 | } | 88 | } |
73 | 89 | ||
90 | /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a | ||
91 | /// block. | ||
92 | fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> { | ||
93 | if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) { | ||
94 | return None; | ||
95 | } | ||
96 | |||
97 | let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?; | ||
98 | |||
99 | // Remove the `{` to get a better parse tree, and reparse | ||
100 | let file = file.reparse(&Indel::delete(brace_token.text_range())); | ||
101 | |||
102 | let mut expr: ast::Expr = find_node_at_offset(file.tree().syntax(), offset)?; | ||
103 | if expr.syntax().text_range().start() != offset { | ||
104 | return None; | ||
105 | } | ||
106 | |||
107 | // Enclose the outermost expression starting at `offset` | ||
108 | while let Some(parent) = expr.syntax().parent() { | ||
109 | if parent.text_range().start() != expr.syntax().text_range().start() { | ||
110 | break; | ||
111 | } | ||
112 | |||
113 | match ast::Expr::cast(parent) { | ||
114 | Some(parent) => expr = parent, | ||
115 | None => break, | ||
116 | } | ||
117 | } | ||
118 | |||
119 | // If it's a statement in a block, we don't know how many statements should be included | ||
120 | if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) { | ||
121 | return None; | ||
122 | } | ||
123 | |||
124 | // Insert `}` right after the expression. | ||
125 | Some(TextEdit::insert(expr.syntax().text_range().end() + TextSize::of("{"), "}".to_string())) | ||
126 | } | ||
127 | |||
74 | /// Returns an edit which should be applied after `=` was typed. Primarily, | 128 | /// Returns an edit which should be applied after `=` was typed. Primarily, |
75 | /// this works when adding `let =`. | 129 | /// this works when adding `let =`. |
76 | // FIXME: use a snippet completion instead of this hack here. | 130 | // FIXME: use a snippet completion instead of this hack here. |
77 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | 131 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
78 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | 132 | if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) { |
133 | return None; | ||
134 | } | ||
79 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | 135 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; |
80 | if let_stmt.semicolon_token().is_some() { | 136 | if let_stmt.semicolon_token().is_some() { |
81 | return None; | 137 | return None; |
@@ -97,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | |||
97 | 153 | ||
98 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | 154 | /// 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> { | 155 | fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
100 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); | 156 | if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) { |
157 | return None; | ||
158 | } | ||
101 | let whitespace = | 159 | let whitespace = |
102 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; | 160 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; |
103 | 161 | ||
@@ -126,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | |||
126 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` | 184 | /// 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> { | 185 | fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
128 | let file_text = file.syntax().text(); | 186 | let file_text = file.syntax().text(); |
129 | assert_eq!(file_text.char_at(offset), Some('>')); | 187 | if !stdx::always!(file_text.char_at(offset) == Some('>')) { |
188 | return None; | ||
189 | } | ||
130 | let after_arrow = offset + TextSize::of('>'); | 190 | let after_arrow = offset + TextSize::of('>'); |
131 | if file_text.char_at(after_arrow) != Some('{') { | 191 | if file_text.char_at(after_arrow) != Some('{') { |
132 | return None; | 192 | return None; |
@@ -149,7 +209,7 @@ mod tests { | |||
149 | let edit = TextEdit::insert(offset, char_typed.to_string()); | 209 | let edit = TextEdit::insert(offset, char_typed.to_string()); |
150 | edit.apply(&mut before); | 210 | edit.apply(&mut before); |
151 | let parse = SourceFile::parse(&before); | 211 | let parse = SourceFile::parse(&before); |
152 | on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| { | 212 | on_char_typed_inner(&parse, offset, char_typed).map(|it| { |
153 | it.apply(&mut before); | 213 | it.apply(&mut before); |
154 | before.to_string() | 214 | before.to_string() |
155 | }) | 215 | }) |
@@ -162,8 +222,8 @@ mod tests { | |||
162 | assert_eq_text!(ra_fixture_after, &actual); | 222 | assert_eq_text!(ra_fixture_after, &actual); |
163 | } | 223 | } |
164 | 224 | ||
165 | fn type_char_noop(char_typed: char, before: &str) { | 225 | fn type_char_noop(char_typed: char, ra_fixture_before: &str) { |
166 | let file_change = do_type_char(char_typed, before); | 226 | let file_change = do_type_char(char_typed, ra_fixture_before); |
167 | assert!(file_change.is_none()) | 227 | assert!(file_change.is_none()) |
168 | } | 228 | } |
169 | 229 | ||
@@ -180,16 +240,16 @@ mod tests { | |||
180 | // "); | 240 | // "); |
181 | type_char( | 241 | type_char( |
182 | '=', | 242 | '=', |
183 | r" | 243 | r#" |
184 | fn foo() { | 244 | fn foo() { |
185 | let foo $0 1 + 1 | 245 | let foo $0 1 + 1 |
186 | } | 246 | } |
187 | ", | 247 | "#, |
188 | r" | 248 | r#" |
189 | fn foo() { | 249 | fn foo() { |
190 | let foo = 1 + 1; | 250 | let foo = 1 + 1; |
191 | } | 251 | } |
192 | ", | 252 | "#, |
193 | ); | 253 | ); |
194 | // do_check(r" | 254 | // do_check(r" |
195 | // fn foo() { | 255 | // fn foo() { |
@@ -208,27 +268,27 @@ fn foo() { | |||
208 | fn indents_new_chain_call() { | 268 | fn indents_new_chain_call() { |
209 | type_char( | 269 | type_char( |
210 | '.', | 270 | '.', |
211 | r" | 271 | r#" |
212 | fn main() { | 272 | fn main() { |
213 | xs.foo() | 273 | xs.foo() |
214 | $0 | 274 | $0 |
215 | } | 275 | } |
216 | ", | 276 | "#, |
217 | r" | 277 | r#" |
218 | fn main() { | 278 | fn main() { |
219 | xs.foo() | 279 | xs.foo() |
220 | . | 280 | . |
221 | } | 281 | } |
222 | ", | 282 | "#, |
223 | ); | 283 | ); |
224 | type_char_noop( | 284 | type_char_noop( |
225 | '.', | 285 | '.', |
226 | r" | 286 | r#" |
227 | fn main() { | 287 | fn main() { |
228 | xs.foo() | 288 | xs.foo() |
229 | $0 | 289 | $0 |
230 | } | 290 | } |
231 | ", | 291 | "#, |
232 | ) | 292 | ) |
233 | } | 293 | } |
234 | 294 | ||
@@ -237,26 +297,26 @@ fn foo() { | |||
237 | type_char( | 297 | type_char( |
238 | '.', | 298 | '.', |
239 | r" | 299 | r" |
240 | fn main() { | 300 | fn main() { |
241 | xs.foo() | 301 | xs.foo() |
242 | $0; | 302 | $0; |
243 | } | 303 | } |
244 | ", | ||
245 | r" | ||
246 | fn main() { | ||
247 | xs.foo() | ||
248 | .; | ||
249 | } | ||
250 | ", | 304 | ", |
305 | r#" | ||
306 | fn main() { | ||
307 | xs.foo() | ||
308 | .; | ||
309 | } | ||
310 | "#, | ||
251 | ); | 311 | ); |
252 | type_char_noop( | 312 | type_char_noop( |
253 | '.', | 313 | '.', |
254 | r" | 314 | r#" |
255 | fn main() { | 315 | fn main() { |
256 | xs.foo() | 316 | xs.foo() |
257 | $0; | 317 | $0; |
258 | } | 318 | } |
259 | ", | 319 | "#, |
260 | ) | 320 | ) |
261 | } | 321 | } |
262 | 322 | ||
@@ -285,30 +345,30 @@ fn main() { | |||
285 | fn indents_continued_chain_call() { | 345 | fn indents_continued_chain_call() { |
286 | type_char( | 346 | type_char( |
287 | '.', | 347 | '.', |
288 | r" | 348 | r#" |
289 | fn main() { | 349 | fn main() { |
290 | xs.foo() | 350 | xs.foo() |
291 | .first() | 351 | .first() |
292 | $0 | 352 | $0 |
293 | } | 353 | } |
294 | ", | 354 | "#, |
295 | r" | 355 | r#" |
296 | fn main() { | 356 | fn main() { |
297 | xs.foo() | 357 | xs.foo() |
298 | .first() | 358 | .first() |
299 | . | 359 | . |
300 | } | 360 | } |
301 | ", | 361 | "#, |
302 | ); | 362 | ); |
303 | type_char_noop( | 363 | type_char_noop( |
304 | '.', | 364 | '.', |
305 | r" | 365 | r#" |
306 | fn main() { | 366 | fn main() { |
307 | xs.foo() | 367 | xs.foo() |
308 | .first() | 368 | .first() |
309 | $0 | 369 | $0 |
310 | } | 370 | } |
311 | ", | 371 | "#, |
312 | ); | 372 | ); |
313 | } | 373 | } |
314 | 374 | ||
@@ -316,33 +376,33 @@ fn main() { | |||
316 | fn indents_middle_of_chain_call() { | 376 | fn indents_middle_of_chain_call() { |
317 | type_char( | 377 | type_char( |
318 | '.', | 378 | '.', |
319 | r" | 379 | r#" |
320 | fn source_impl() { | 380 | fn source_impl() { |
321 | let var = enum_defvariant_list().unwrap() | 381 | let var = enum_defvariant_list().unwrap() |
322 | $0 | 382 | $0 |
323 | .nth(92) | 383 | .nth(92) |
324 | .unwrap(); | 384 | .unwrap(); |
325 | } | 385 | } |
326 | ", | 386 | "#, |
327 | r" | 387 | r#" |
328 | fn source_impl() { | 388 | fn source_impl() { |
329 | let var = enum_defvariant_list().unwrap() | 389 | let var = enum_defvariant_list().unwrap() |
330 | . | 390 | . |
331 | .nth(92) | 391 | .nth(92) |
332 | .unwrap(); | 392 | .unwrap(); |
333 | } | 393 | } |
334 | ", | 394 | "#, |
335 | ); | 395 | ); |
336 | type_char_noop( | 396 | type_char_noop( |
337 | '.', | 397 | '.', |
338 | r" | 398 | r#" |
339 | fn source_impl() { | 399 | fn source_impl() { |
340 | let var = enum_defvariant_list().unwrap() | 400 | let var = enum_defvariant_list().unwrap() |
341 | $0 | 401 | $0 |
342 | .nth(92) | 402 | .nth(92) |
343 | .unwrap(); | 403 | .unwrap(); |
344 | } | 404 | } |
345 | ", | 405 | "#, |
346 | ); | 406 | ); |
347 | } | 407 | } |
348 | 408 | ||
@@ -350,24 +410,113 @@ fn main() { | |||
350 | fn dont_indent_freestanding_dot() { | 410 | fn dont_indent_freestanding_dot() { |
351 | type_char_noop( | 411 | type_char_noop( |
352 | '.', | 412 | '.', |
353 | r" | 413 | r#" |
354 | fn main() { | 414 | fn main() { |
355 | $0 | 415 | $0 |
356 | } | 416 | } |
357 | ", | 417 | "#, |
358 | ); | 418 | ); |
359 | type_char_noop( | 419 | type_char_noop( |
360 | '.', | 420 | '.', |
361 | r" | 421 | r#" |
362 | fn main() { | 422 | fn main() { |
363 | $0 | 423 | $0 |
364 | } | 424 | } |
365 | ", | 425 | "#, |
366 | ); | 426 | ); |
367 | } | 427 | } |
368 | 428 | ||
369 | #[test] | 429 | #[test] |
370 | fn adds_space_after_return_type() { | 430 | fn adds_space_after_return_type() { |
371 | type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") | 431 | type_char( |
432 | '>', | ||
433 | r#" | ||
434 | fn foo() -$0{ 92 } | ||
435 | "#, | ||
436 | r#" | ||
437 | fn foo() -> { 92 } | ||
438 | "#, | ||
439 | ); | ||
440 | } | ||
441 | |||
442 | #[test] | ||
443 | fn adds_closing_brace() { | ||
444 | type_char( | ||
445 | '{', | ||
446 | r#" | ||
447 | fn f() { match () { _ => $0() } } | ||
448 | "#, | ||
449 | r#" | ||
450 | fn f() { match () { _ => {()} } } | ||
451 | "#, | ||
452 | ); | ||
453 | type_char( | ||
454 | '{', | ||
455 | r#" | ||
456 | fn f() { $0() } | ||
457 | "#, | ||
458 | r#" | ||
459 | fn f() { {()} } | ||
460 | "#, | ||
461 | ); | ||
462 | type_char( | ||
463 | '{', | ||
464 | r#" | ||
465 | fn f() { let x = $0(); } | ||
466 | "#, | ||
467 | r#" | ||
468 | fn f() { let x = {()}; } | ||
469 | "#, | ||
470 | ); | ||
471 | type_char( | ||
472 | '{', | ||
473 | r#" | ||
474 | fn f() { let x = $0a.b(); } | ||
475 | "#, | ||
476 | r#" | ||
477 | fn f() { let x = {a.b()}; } | ||
478 | "#, | ||
479 | ); | ||
480 | type_char( | ||
481 | '{', | ||
482 | r#" | ||
483 | const S: () = $0(); | ||
484 | fn f() {} | ||
485 | "#, | ||
486 | r#" | ||
487 | const S: () = {()}; | ||
488 | fn f() {} | ||
489 | "#, | ||
490 | ); | ||
491 | type_char( | ||
492 | '{', | ||
493 | r#" | ||
494 | const S: () = $0a.b(); | ||
495 | fn f() {} | ||
496 | "#, | ||
497 | r#" | ||
498 | const S: () = {a.b()}; | ||
499 | fn f() {} | ||
500 | "#, | ||
501 | ); | ||
502 | type_char( | ||
503 | '{', | ||
504 | r#" | ||
505 | fn f() { | ||
506 | match x { | ||
507 | 0 => $0(), | ||
508 | 1 => (), | ||
509 | } | ||
510 | } | ||
511 | "#, | ||
512 | r#" | ||
513 | fn f() { | ||
514 | match x { | ||
515 | 0 => {()}, | ||
516 | 1 => (), | ||
517 | } | ||
518 | } | ||
519 | "#, | ||
520 | ); | ||
372 | } | 521 | } |
373 | } | 522 | } |