@@ -22,25 +22,27 @@ use ide_db::{
22
22
use syntax:: {
23
23
algo:: find_node_at_offset,
24
24
ast:: { self , edit:: IndentLevel , AstToken } ,
25
- AstNode , SourceFile ,
25
+ AstNode , Parse , SourceFile ,
26
26
SyntaxKind :: { FIELD_EXPR , METHOD_CALL_EXPR } ,
27
27
TextRange , TextSize ,
28
28
} ;
29
29
30
- use text_edit:: TextEdit ;
30
+ use text_edit:: { Indel , TextEdit } ;
31
31
32
32
use crate :: SourceChange ;
33
33
34
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
39
// Feature: On Typing Assists
39
40
//
40
41
// Some features trigger on typing certain characters:
41
42
//
42
43
// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
43
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
47
// VS Code::
46
48
//
@@ -57,28 +59,79 @@ pub(crate) fn on_char_typed(
57
59
position : FilePosition ,
58
60
char_typed : char ,
59
61
) -> Option < SourceChange > {
60
- assert ! ( TRIGGER_CHARS . contains( char_typed) ) ;
61
- let file = & db. parse ( position. file_id ) . tree ( ) ;
62
- assert_eq ! ( file. syntax( ) . text( ) . char_at( position. offset) , Some ( char_typed) ) ;
62
+ if !stdx:: always!( TRIGGER_CHARS . contains( char_typed) ) {
63
+ return None ;
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
+ }
63
69
let edit = on_char_typed_inner ( file, position. offset , char_typed) ?;
64
70
Some ( SourceChange :: from_text_edit ( position. file_id , edit) )
65
71
}
66
72
67
- fn on_char_typed_inner ( file : & SourceFile , offset : TextSize , char_typed : char ) -> Option < TextEdit > {
68
- assert ! ( TRIGGER_CHARS . contains( char_typed) ) ;
73
+ fn on_char_typed_inner (
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
+ }
69
81
match char_typed {
70
- '.' => on_dot_typed ( file, offset) ,
71
- '=' => on_eq_typed ( file, offset) ,
72
- '>' => on_arrow_typed ( file, offset) ,
82
+ '.' => on_dot_typed ( & file. tree ( ) , offset) ,
83
+ '=' => on_eq_typed ( & file. tree ( ) , offset) ,
84
+ '>' => on_arrow_typed ( & file. tree ( ) , offset) ,
85
+ '{' => on_opening_brace_typed ( file, offset) ,
73
86
_ => unreachable ! ( ) ,
74
87
}
75
88
}
76
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
+
77
128
/// Returns an edit which should be applied after `=` was typed. Primarily,
78
129
/// this works when adding `let =`.
79
130
// FIXME: use a snippet completion instead of this hack here.
80
131
fn on_eq_typed ( file : & SourceFile , offset : TextSize ) -> Option < TextEdit > {
81
- assert_eq ! ( file. syntax( ) . text( ) . char_at( offset) , Some ( '=' ) ) ;
132
+ if !stdx:: always!( file. syntax( ) . text( ) . char_at( offset) == Some ( '=' ) ) {
133
+ return None ;
134
+ }
82
135
let let_stmt: ast:: LetStmt = find_node_at_offset ( file. syntax ( ) , offset) ?;
83
136
if let_stmt. semicolon_token ( ) . is_some ( ) {
84
137
return None ;
@@ -100,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
100
153
101
154
/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
102
155
fn on_dot_typed ( file : & SourceFile , offset : TextSize ) -> Option < TextEdit > {
103
- assert_eq ! ( file. syntax( ) . text( ) . char_at( offset) , Some ( '.' ) ) ;
156
+ if !stdx:: always!( file. syntax( ) . text( ) . char_at( offset) == Some ( '.' ) ) {
157
+ return None ;
158
+ }
104
159
let whitespace =
105
160
file. syntax ( ) . token_at_offset ( offset) . left_biased ( ) . and_then ( ast:: Whitespace :: cast) ?;
106
161
@@ -129,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
129
184
/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
130
185
fn on_arrow_typed ( file : & SourceFile , offset : TextSize ) -> Option < TextEdit > {
131
186
let file_text = file. syntax ( ) . text ( ) ;
132
- assert_eq ! ( file_text. char_at( offset) , Some ( '>' ) ) ;
187
+ if !stdx:: always!( file_text. char_at( offset) == Some ( '>' ) ) {
188
+ return None ;
189
+ }
133
190
let after_arrow = offset + TextSize :: of ( '>' ) ;
134
191
if file_text. char_at ( after_arrow) != Some ( '{' ) {
135
192
return None ;
@@ -152,7 +209,7 @@ mod tests {
152
209
let edit = TextEdit :: insert ( offset, char_typed. to_string ( ) ) ;
153
210
edit. apply ( & mut before) ;
154
211
let parse = SourceFile :: parse ( & before) ;
155
- on_char_typed_inner ( & parse. tree ( ) , offset, char_typed) . map ( |it| {
212
+ on_char_typed_inner ( & parse, offset, char_typed) . map ( |it| {
156
213
it. apply ( & mut before) ;
157
214
before. to_string ( )
158
215
} )
@@ -373,4 +430,85 @@ fn main() {
373
430
fn adds_space_after_return_type ( ) {
374
431
type_char ( '>' , "fn foo() -$0{ 92 }" , "fn foo() -> { 92 }" )
375
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
+ }
376
514
}
0 commit comments