@@ -22,25 +22,27 @@ use ide_db::{
2222use syntax:: {
2323 algo:: find_node_at_offset,
2424 ast:: { self , edit:: IndentLevel , AstToken } ,
25- AstNode , SourceFile ,
25+ AstNode , Parse , SourceFile ,
2626 SyntaxKind :: { FIELD_EXPR , METHOD_CALL_EXPR } ,
2727 TextRange , TextSize ,
2828} ;
2929
30- use text_edit:: TextEdit ;
30+ use text_edit:: { Indel , TextEdit } ;
3131
3232use crate :: SourceChange ;
3333
3434pub ( crate ) use on_enter:: on_enter;
3535
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 = ".=>{" ;
3738
3839// Feature: On Typing Assists
3940//
4041// Some features trigger on typing certain characters:
4142//
4243// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
4344// - typing `.` in a chain method call auto-indents
45+ // - typing `{` in front of an expression inserts a closing `}` after the expression
4446//
4547// VS Code::
4648//
@@ -57,28 +59,79 @@ pub(crate) fn on_char_typed(
5759 position : FilePosition ,
5860 char_typed : char ,
5961) -> 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+ }
6369 let edit = on_char_typed_inner ( file, position. offset , char_typed) ?;
6470 Some ( SourceChange :: from_text_edit ( position. file_id , edit) )
6571}
6672
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+ }
6981 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) ,
7386 _ => unreachable ! ( ) ,
7487 }
7588}
7689
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+
77128/// Returns an edit which should be applied after `=` was typed. Primarily,
78129/// this works when adding `let =`.
79130// FIXME: use a snippet completion instead of this hack here.
80131fn 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+ }
82135 let let_stmt: ast:: LetStmt = find_node_at_offset ( file. syntax ( ) , offset) ?;
83136 if let_stmt. semicolon_token ( ) . is_some ( ) {
84137 return None ;
@@ -100,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
100153
101154/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
102155fn 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+ }
104159 let whitespace =
105160 file. syntax ( ) . token_at_offset ( offset) . left_biased ( ) . and_then ( ast:: Whitespace :: cast) ?;
106161
@@ -129,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
129184/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
130185fn on_arrow_typed ( file : & SourceFile , offset : TextSize ) -> Option < TextEdit > {
131186 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+ }
133190 let after_arrow = offset + TextSize :: of ( '>' ) ;
134191 if file_text. char_at ( after_arrow) != Some ( '{' ) {
135192 return None ;
@@ -152,7 +209,7 @@ mod tests {
152209 let edit = TextEdit :: insert ( offset, char_typed. to_string ( ) ) ;
153210 edit. apply ( & mut before) ;
154211 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| {
156213 it. apply ( & mut before) ;
157214 before. to_string ( )
158215 } )
@@ -373,4 +430,85 @@ fn main() {
373430 fn adds_space_after_return_type ( ) {
374431 type_char ( '>' , "fn foo() -$0{ 92 }" , "fn foo() -> { 92 }" )
375432 }
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+ }
376514}
0 commit comments