diff options
Diffstat (limited to 'crates/ide/src/typing.rs')
-rw-r--r-- | crates/ide/src/typing.rs | 171 |
1 files changed, 156 insertions, 15 deletions
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index e10b7d98e..1378048e5 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 | }) |
@@ -370,4 +430,85 @@ fn main() { | |||
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('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") |
372 | } | 432 | } |
433 | |||
434 | #[test] | ||
435 | fn adds_closing_brace() { | ||
436 | type_char( | ||
437 | '{', | ||
438 | r#" | ||
439 | fn f() { match () { _ => $0() } } | ||
440 | "#, | ||
441 | r#" | ||
442 | fn f() { match () { _ => {()} } } | ||
443 | "#, | ||
444 | ); | ||
445 | type_char( | ||
446 | '{', | ||
447 | r#" | ||
448 | fn f() { $0() } | ||
449 | "#, | ||
450 | r#" | ||
451 | fn f() { {()} } | ||
452 | "#, | ||
453 | ); | ||
454 | type_char( | ||
455 | '{', | ||
456 | r#" | ||
457 | fn f() { let x = $0(); } | ||
458 | "#, | ||
459 | r#" | ||
460 | fn f() { let x = {()}; } | ||
461 | "#, | ||
462 | ); | ||
463 | type_char( | ||
464 | '{', | ||
465 | r#" | ||
466 | fn f() { let x = $0a.b(); } | ||
467 | "#, | ||
468 | r#" | ||
469 | fn f() { let x = {a.b()}; } | ||
470 | "#, | ||
471 | ); | ||
472 | type_char( | ||
473 | '{', | ||
474 | r#" | ||
475 | const S: () = $0(); | ||
476 | fn f() {} | ||
477 | "#, | ||
478 | r#" | ||
479 | const S: () = {()}; | ||
480 | fn f() {} | ||
481 | "#, | ||
482 | ); | ||
483 | type_char( | ||
484 | '{', | ||
485 | r#" | ||
486 | const S: () = $0a.b(); | ||
487 | fn f() {} | ||
488 | "#, | ||
489 | r#" | ||
490 | const S: () = {a.b()}; | ||
491 | fn f() {} | ||
492 | "#, | ||
493 | ); | ||
494 | type_char( | ||
495 | '{', | ||
496 | r#" | ||
497 | fn f() { | ||
498 | match x { | ||
499 | 0 => $0(), | ||
500 | 1 => (), | ||
501 | } | ||
502 | } | ||
503 | "#, | ||
504 | r#" | ||
505 | fn f() { | ||
506 | match x { | ||
507 | 0 => {()}, | ||
508 | 1 => (), | ||
509 | } | ||
510 | } | ||
511 | "#, | ||
512 | ); | ||
513 | } | ||
373 | } | 514 | } |