From 36cd724b7b146c33804db4b110111ad71be9cb72 Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Tue, 6 Apr 2021 23:55:39 +0200 Subject: Autoclose blocks when typing `{` --- crates/ide/src/typing.rs | 47 +++++++++++++++++++++++++++++++++++- crates/rust-analyzer/src/caps.rs | 2 +- crates/rust-analyzer/src/handlers.rs | 1 - 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index 11408d445..de65632e3 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -33,7 +33,8 @@ use crate::SourceChange; pub(crate) use on_enter::on_enter; -pub(crate) const TRIGGER_CHARS: &str = ".=>"; +// Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`. +pub(crate) const TRIGGER_CHARS: &str = ".=>{"; // Feature: On Typing Assists // @@ -70,10 +71,47 @@ fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> '.' => on_dot_typed(file, offset), '=' => on_eq_typed(file, offset), '>' => on_arrow_typed(file, offset), + '{' => on_opening_brace_typed(file, offset), _ => unreachable!(), } } +/// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a +/// block. +fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option { + assert_eq!(file.syntax().text().char_at(offset), Some('{')); + let brace_token = file.syntax().token_at_offset(offset).right_biased()?; + let block = ast::BlockExpr::cast(brace_token.parent()?)?; + + // We expect a block expression enclosing exactly 1 preexisting expression. It can be parsed as + // either the trailing expr or an ExprStmt. + let offset = { + match block.tail_expr() { + Some(expr) => { + if block.statements().next().is_some() { + return None; + } + expr.syntax().text_range().end() + } + None => { + if block.statements().count() != 1 { + return None; + } + + match block.statements().next()? { + ast::Stmt::ExprStmt(it) => { + // Use the expression span to place `}` before the `;` + it.expr()?.syntax().text_range().end() + } + _ => return None, + } + } + } + }; + + Some(TextEdit::insert(offset, "}".to_string())) +} + /// Returns an edit which should be applied after `=` was typed. Primarily, /// this works when adding `let =`. // FIXME: use a snippet completion instead of this hack here. @@ -373,4 +411,11 @@ fn main() { fn adds_space_after_return_type() { type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") } + + #[test] + fn adds_closing_brace() { + type_char('{', r"fn f() { match () { _ => $0() } }", r"fn f() { match () { _ => {()} } }"); + type_char('{', r"fn f() { $0(); }", r"fn f() { {()}; }"); + type_char('{', r"fn f() { let x = $0(); }", r"fn f() { let x = {()}; }"); + } } diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs index 7a5bcb8c7..3c87782f2 100644 --- a/crates/rust-analyzer/src/caps.rs +++ b/crates/rust-analyzer/src/caps.rs @@ -57,7 +57,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti document_range_formatting_provider: None, document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { first_trigger_character: "=".to_string(), - more_trigger_character: Some(vec![".".to_string(), ">".to_string()]), + more_trigger_character: Some(vec![".".to_string(), ">".to_string(), "{".to_string()]), }), selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index 4d10a2ead..31d8c487b 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -231,7 +231,6 @@ pub(crate) fn handle_on_enter( Ok(Some(edit)) } -// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`. pub(crate) fn handle_on_type_formatting( snap: GlobalStateSnapshot, params: lsp_types::DocumentOnTypeFormattingParams, -- cgit v1.2.3 From 61e292fab1a5c5f3c97ace967268b6197a687ae1 Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Wed, 7 Apr 2021 01:24:24 +0200 Subject: Complete braces more aggressively --- crates/ide/src/typing.rs | 55 ++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index de65632e3..b0234d7fd 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -86,26 +86,13 @@ fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option { - if block.statements().next().is_some() { - return None; - } - expr.syntax().text_range().end() - } - None => { - if block.statements().count() != 1 { - return None; - } - - match block.statements().next()? { - ast::Stmt::ExprStmt(it) => { - // Use the expression span to place `}` before the `;` - it.expr()?.syntax().text_range().end() - } - _ => return None, - } - } + match block.statements().next() { + Some(ast::Stmt::ExprStmt(it)) => { + // Use the expression span to place `}` before the `;` + it.expr()?.syntax().text_range().end() + }, + None => block.tail_expr()?.syntax().text_range().end(), + _ => return None, } }; @@ -417,5 +404,33 @@ fn main() { type_char('{', r"fn f() { match () { _ => $0() } }", r"fn f() { match () { _ => {()} } }"); type_char('{', r"fn f() { $0(); }", r"fn f() { {()}; }"); type_char('{', r"fn f() { let x = $0(); }", r"fn f() { let x = {()}; }"); + type_char( + '{', + r" + const S: () = $0(); + fn f() {} + ", + r" + const S: () = {()}; + fn f() {} + ", + ); + type_char( + '{', + r" + fn f() { + match x { + 0 => $0(), + 1 => (), + } + }", + r" + fn f() { + match x { + 0 => {()}, + 1 => (), + } + }", + ); } } -- cgit v1.2.3 From d75cacc601891eb89318b3c483b3fcfb0535f0f8 Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Wed, 7 Apr 2021 01:26:22 +0200 Subject: Use stdx::always --- crates/ide/src/typing.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index b0234d7fd..c1bdc51b0 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -79,7 +79,7 @@ fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a /// block. fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option { - assert_eq!(file.syntax().text().char_at(offset), Some('{')); + stdx::always!(file.syntax().text().char_at(offset) == Some('{')); let brace_token = file.syntax().token_at_offset(offset).right_biased()?; let block = ast::BlockExpr::cast(brace_token.parent()?)?; @@ -90,7 +90,7 @@ fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option { // Use the expression span to place `}` before the `;` it.expr()?.syntax().text_range().end() - }, + } None => block.tail_expr()?.syntax().text_range().end(), _ => return None, } @@ -103,7 +103,7 @@ fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option Option { - assert_eq!(file.syntax().text().char_at(offset), Some('=')); + stdx::always!(file.syntax().text().char_at(offset) == Some('=')); let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; if let_stmt.semicolon_token().is_some() { return None; @@ -125,7 +125,7 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option { /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option { - assert_eq!(file.syntax().text().char_at(offset), Some('.')); + stdx::always!(file.syntax().text().char_at(offset) == Some('.')); let whitespace = file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; @@ -154,7 +154,7 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option { /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option { let file_text = file.syntax().text(); - assert_eq!(file_text.char_at(offset), Some('>')); + stdx::always!(file_text.char_at(offset) == Some('>')); let after_arrow = offset + TextSize::of('>'); if file_text.char_at(after_arrow) != Some('{') { return None; -- cgit v1.2.3 From 17a1011a12d270dfaf83a404dd7c40c5d9967064 Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Wed, 7 Apr 2021 01:39:17 +0200 Subject: simplify --- crates/ide/src/typing.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index c1bdc51b0..391a8e867 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -85,15 +85,13 @@ fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option { - // Use the expression span to place `}` before the `;` - it.expr()?.syntax().text_range().end() - } - None => block.tail_expr()?.syntax().text_range().end(), - _ => return None, + let offset = match block.statements().next() { + Some(ast::Stmt::ExprStmt(it)) => { + // Use the expression span to place `}` before the `;` + it.expr()?.syntax().text_range().end() } + None => block.tail_expr()?.syntax().text_range().end(), + _ => return None, }; Some(TextEdit::insert(offset, "}".to_string())) -- cgit v1.2.3 From 3f599ae4ed93691c07bc24030a3c0d9e8508ed4a Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Wed, 7 Apr 2021 16:37:47 +0200 Subject: Rewrite, reparse modified file --- crates/ide/src/typing.rs | 163 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 114 insertions(+), 49 deletions(-) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index 391a8e867..9050853ce 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -22,12 +22,12 @@ use ide_db::{ use syntax::{ algo::find_node_at_offset, ast::{self, edit::IndentLevel, AstToken}, - AstNode, SourceFile, + AstNode, Parse, SourceFile, SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, TextRange, TextSize, }; -use text_edit::TextEdit; +use text_edit::{Indel, TextEdit}; use crate::SourceChange; @@ -59,18 +59,22 @@ pub(crate) fn on_char_typed( char_typed: char, ) -> Option { assert!(TRIGGER_CHARS.contains(char_typed)); - let file = &db.parse(position.file_id).tree(); - assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); + let file = &db.parse(position.file_id); + assert_eq!(file.tree().syntax().text().char_at(position.offset), Some(char_typed)); let edit = on_char_typed_inner(file, position.offset, char_typed)?; Some(SourceChange::from_text_edit(position.file_id, edit)) } -fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option { +fn on_char_typed_inner( + file: &Parse, + offset: TextSize, + char_typed: char, +) -> Option { assert!(TRIGGER_CHARS.contains(char_typed)); match char_typed { - '.' => on_dot_typed(file, offset), - '=' => on_eq_typed(file, offset), - '>' => on_arrow_typed(file, offset), + '.' => on_dot_typed(&file.tree(), offset), + '=' => on_eq_typed(&file.tree(), offset), + '>' => on_arrow_typed(&file.tree(), offset), '{' => on_opening_brace_typed(file, offset), _ => unreachable!(), } @@ -78,23 +82,38 @@ fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a /// block. -fn on_opening_brace_typed(file: &SourceFile, offset: TextSize) -> Option { - stdx::always!(file.syntax().text().char_at(offset) == Some('{')); - let brace_token = file.syntax().token_at_offset(offset).right_biased()?; - let block = ast::BlockExpr::cast(brace_token.parent()?)?; - - // We expect a block expression enclosing exactly 1 preexisting expression. It can be parsed as - // either the trailing expr or an ExprStmt. - let offset = match block.statements().next() { - Some(ast::Stmt::ExprStmt(it)) => { - // Use the expression span to place `}` before the `;` - it.expr()?.syntax().text_range().end() +fn on_opening_brace_typed(file: &Parse, offset: TextSize) -> Option { + stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')); + + let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?; + + // Remove the `{` to get a better parse tree, and reparse + let file = file.reparse(&Indel::delete(brace_token.text_range())); + + let mut expr: ast::Expr = find_node_at_offset(file.tree().syntax(), offset)?; + if expr.syntax().text_range().start() != offset { + return None; + } + + // Enclose the outermost expression starting at `offset` + while let Some(parent) = expr.syntax().parent() { + if parent.text_range().start() != expr.syntax().text_range().start() { + break; } - None => block.tail_expr()?.syntax().text_range().end(), - _ => return None, - }; - Some(TextEdit::insert(offset, "}".to_string())) + match ast::Expr::cast(parent) { + Some(parent) => expr = parent, + None => break, + } + } + + // If it's a statement in a block, we don't know how many statements should be included + if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) { + return None; + } + + // Insert `}` right after the expression. + Some(TextEdit::insert(expr.syntax().text_range().end() + TextSize::of("{"), "}".to_string())) } /// Returns an edit which should be applied after `=` was typed. Primarily, @@ -175,7 +194,7 @@ mod tests { let edit = TextEdit::insert(offset, char_typed.to_string()); edit.apply(&mut before); let parse = SourceFile::parse(&before); - on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| { + on_char_typed_inner(&parse, offset, char_typed).map(|it| { it.apply(&mut before); before.to_string() }) @@ -399,36 +418,82 @@ fn main() { #[test] fn adds_closing_brace() { - type_char('{', r"fn f() { match () { _ => $0() } }", r"fn f() { match () { _ => {()} } }"); - type_char('{', r"fn f() { $0(); }", r"fn f() { {()}; }"); - type_char('{', r"fn f() { let x = $0(); }", r"fn f() { let x = {()}; }"); type_char( '{', - r" - const S: () = $0(); - fn f() {} - ", - r" - const S: () = {()}; - fn f() {} - ", + r#" +fn f() { match () { _ => $0() } } + "#, + r#" +fn f() { match () { _ => {()} } } + "#, ); type_char( '{', - r" - fn f() { - match x { - 0 => $0(), - 1 => (), - } - }", - r" - fn f() { - match x { - 0 => {()}, - 1 => (), - } - }", + r#" +fn f() { $0() } + "#, + r#" +fn f() { {()} } + "#, + ); + type_char( + '{', + r#" +fn f() { let x = $0(); } + "#, + r#" +fn f() { let x = {()}; } + "#, + ); + type_char( + '{', + r#" +fn f() { let x = $0a.b(); } + "#, + r#" +fn f() { let x = {a.b()}; } + "#, + ); + type_char( + '{', + r#" +const S: () = $0(); +fn f() {} + "#, + r#" +const S: () = {()}; +fn f() {} + "#, + ); + type_char( + '{', + r#" +const S: () = $0a.b(); +fn f() {} + "#, + r#" +const S: () = {a.b()}; +fn f() {} + "#, + ); + type_char( + '{', + r#" +fn f() { + match x { + 0 => $0(), + 1 => (), + } +} + "#, + r#" +fn f() { + match x { + 0 => {()}, + 1 => (), + } +} + "#, ); } } -- cgit v1.2.3 From 39d59fb06f8a2ccf35bbd9e7324d3f357a163b26 Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Wed, 7 Apr 2021 16:44:25 +0200 Subject: Make better use of `stdx::always` --- crates/ide/src/typing.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index 9050853ce..809ff7d20 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -58,9 +58,13 @@ pub(crate) fn on_char_typed( position: FilePosition, char_typed: char, ) -> Option { - assert!(TRIGGER_CHARS.contains(char_typed)); + if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { + return None; + } let file = &db.parse(position.file_id); - assert_eq!(file.tree().syntax().text().char_at(position.offset), Some(char_typed)); + if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) { + return None; + } let edit = on_char_typed_inner(file, position.offset, char_typed)?; Some(SourceChange::from_text_edit(position.file_id, edit)) } @@ -70,7 +74,9 @@ fn on_char_typed_inner( offset: TextSize, char_typed: char, ) -> Option { - assert!(TRIGGER_CHARS.contains(char_typed)); + if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) { + return None; + } match char_typed { '.' => on_dot_typed(&file.tree(), offset), '=' => on_eq_typed(&file.tree(), offset), @@ -83,7 +89,9 @@ fn on_char_typed_inner( /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a /// block. fn on_opening_brace_typed(file: &Parse, offset: TextSize) -> Option { - stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')); + if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) { + return None; + } let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?; @@ -120,7 +128,9 @@ fn on_opening_brace_typed(file: &Parse, offset: TextSize) -> Option< /// this works when adding `let =`. // FIXME: use a snippet completion instead of this hack here. fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option { - stdx::always!(file.syntax().text().char_at(offset) == Some('=')); + if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) { + return None; + } let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; if let_stmt.semicolon_token().is_some() { return None; @@ -142,7 +152,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option { /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option { - stdx::always!(file.syntax().text().char_at(offset) == Some('.')); + if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) { + return None; + } let whitespace = file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; @@ -171,7 +183,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option { /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option { let file_text = file.syntax().text(); - stdx::always!(file_text.char_at(offset) == Some('>')); + if !stdx::always!(file_text.char_at(offset) == Some('>')) { + return None; + } let after_arrow = offset + TextSize::of('>'); if file_text.char_at(after_arrow) != Some('{') { return None; -- cgit v1.2.3 From d789cf8f31b3f8281046a352a73e17e15ff3c224 Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Wed, 7 Apr 2021 17:19:42 +0200 Subject: Document `}` insertion --- crates/ide/src/typing.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index 809ff7d20..1378048e5 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -42,6 +42,7 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>{"; // // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression // - typing `.` in a chain method call auto-indents +// - typing `{` in front of an expression inserts a closing `}` after the expression // // VS Code:: // -- cgit v1.2.3