From 2fa90e736b026ee979d9eb59178dc1f792228250 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 28 Aug 2018 11:12:42 +0300 Subject: [PATCH] better recovery for exprs --- crates/libeditor/src/lib.rs | 2 +- crates/libeditor/src/typing.rs | 24 +++++- crates/libeditor/tests/test.rs | 46 ++++++++++- crates/libsyntax2/src/ast/generated.rs | 78 ++++++++++++++++++- crates/libsyntax2/src/ast/mod.rs | 9 +++ crates/libsyntax2/src/grammar.ron | 10 ++- .../src/grammar/expressions/atom.rs | 5 +- crates/libsyntax2/src/parser_api.rs | 19 ++++- .../parser/err/0017_incomplete_binexpr.txt | 5 +- .../data/parser/err/0018_incomplete_fn.txt | 19 +++-- .../tests/data/parser/err/0019_let_recover.rs | 4 + .../data/parser/err/0019_let_recover.txt | 39 ++++++++++ crates/server/src/caps.rs | 6 +- crates/server/src/main_loop/handlers.rs | 19 +++++ crates/server/src/main_loop/mod.rs | 4 + crates/server/src/req.rs | 1 + 16 files changed, 263 insertions(+), 27 deletions(-) create mode 100644 crates/libsyntax2/tests/data/parser/err/0019_let_recover.rs create mode 100644 crates/libsyntax2/tests/data/parser/err/0019_let_recover.txt diff --git a/crates/libeditor/src/lib.rs b/crates/libeditor/src/lib.rs index d39e56d81d..34056b3c0e 100644 --- a/crates/libeditor/src/lib.rs +++ b/crates/libeditor/src/lib.rs @@ -27,7 +27,7 @@ pub use self::{ ActionResult, flip_comma, add_derive, add_impl, }, - typing::join_lines, + typing::{join_lines, on_eq_typed}, completion::scope_completion, }; diff --git a/crates/libeditor/src/typing.rs b/crates/libeditor/src/typing.rs index 060095f28b..48a8d6bb05 100644 --- a/crates/libeditor/src/typing.rs +++ b/crates/libeditor/src/typing.rs @@ -7,11 +7,11 @@ use libsyntax2::{ walk::preorder, find_covering_node, }, - text_utils::intersect, + text_utils::{intersect, contains_offset_nonstrict}, SyntaxKind::*, }; -use {ActionResult, EditBuilder}; +use {ActionResult, EditBuilder, find_node_at_offset}; pub fn join_lines(file: &File, range: TextRange) -> ActionResult { let range = if range.is_empty() { @@ -56,6 +56,26 @@ pub fn join_lines(file: &File, range: TextRange) -> ActionResult { } } +pub fn on_eq_typed(file: &File, offset: TextUnit) -> Option { + let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; + if let_stmt.has_semi() { + return None; + } + if let Some(expr) = let_stmt.initializer() { + let expr_range = expr.syntax().range(); + if contains_offset_nonstrict(expr_range, offset) && offset != expr_range.start() { + return None; + } + } + let offset = let_stmt.syntax().range().end(); + let mut edit = EditBuilder::new(); + edit.insert(offset, ";".to_string()); + Some(ActionResult { + edit: edit.finish(), + cursor_position: None, + }) +} + fn remove_newline( edit: &mut EditBuilder, node: SyntaxNodeRef, diff --git a/crates/libeditor/tests/test.rs b/crates/libeditor/tests/test.rs index d8c24610db..17926d5ae4 100644 --- a/crates/libeditor/tests/test.rs +++ b/crates/libeditor/tests/test.rs @@ -9,7 +9,7 @@ use libeditor::{ ActionResult, highlight, runnables, extend_selection, file_structure, flip_comma, add_derive, add_impl, matching_brace, - join_lines, scope_completion, + join_lines, on_eq_typed, scope_completion, }; #[test] @@ -227,7 +227,7 @@ pub fn reparse(&self, edit: &AtomEdit) -> File { #[test] fn test_join_lines_selection() { fn do_check(before: &str, after: &str) { - let (sel, before) = extract_range(&before); + let (sel, before) = extract_range(before); let file = file(&before); let result = join_lines(&file, sel); let actual = result.edit.apply(&before); @@ -256,6 +256,48 @@ struct Foo { f: u32 } "); } +#[test] +fn test_on_eq_typed() { + fn do_check(before: &str, after: &str) { + let (offset, before) = extract_offset(before); + let file = file(&before); + let result = on_eq_typed(&file, offset).unwrap(); + let actual = result.edit.apply(&before); + assert_eq_text!(after, &actual); + } + + do_check(r" +fn foo() { + let foo =<|> +} +", r" +fn foo() { + let foo =; +} +"); + do_check(r" +fn foo() { + let foo =<|> 1 + 1 +} +", r" +fn foo() { + let foo = 1 + 1; +} +"); +// do_check(r" +// fn foo() { +// let foo =<|> +// let bar = 1; +// } +// ", r" +// fn foo() { +// let foo =; +// let bar = 1; +// } +// "); + +} + #[test] fn test_completion() { fn do_check(code: &str, expected_completions: &str) { diff --git a/crates/libsyntax2/src/ast/generated.rs b/crates/libsyntax2/src/ast/generated.rs index f99d1274ab..6181aada8c 100644 --- a/crates/libsyntax2/src/ast/generated.rs +++ b/crates/libsyntax2/src/ast/generated.rs @@ -439,6 +439,24 @@ impl<'a> ExprStmt<'a> { } } +// ExternCrateItem +#[derive(Debug, Clone, Copy)] +pub struct ExternCrateItem<'a> { + syntax: SyntaxNodeRef<'a>, +} + +impl<'a> AstNode<'a> for ExternCrateItem<'a> { + fn cast(syntax: SyntaxNodeRef<'a>) -> Option { + match syntax.kind() { + EXTERN_CRATE_ITEM => Some(ExternCrateItem { syntax }), + _ => None, + } + } + fn syntax(self) -> SyntaxNodeRef<'a> { self.syntax } +} + +impl<'a> ExternCrateItem<'a> {} + // FieldExpr #[derive(Debug, Clone, Copy)] pub struct FieldExpr<'a> { @@ -839,11 +857,51 @@ impl<'a> AstNode<'a> for Module<'a> { impl<'a> ast::NameOwner<'a> for Module<'a> {} impl<'a> ast::AttrsOwner<'a> for Module<'a> {} impl<'a> Module<'a> { - pub fn modules(self) -> impl Iterator> + 'a { + pub fn items(self) -> impl Iterator> + 'a { super::children(self) } } +// ModuleItem +#[derive(Debug, Clone, Copy)] +pub enum ModuleItem<'a> { + StructDef(StructDef<'a>), + EnumDef(EnumDef<'a>), + FnDef(FnDef<'a>), + TraitDef(TraitDef<'a>), + ImplItem(ImplItem<'a>), + UseItem(UseItem<'a>), + ExternCrateItem(ExternCrateItem<'a>), +} + +impl<'a> AstNode<'a> for ModuleItem<'a> { + fn cast(syntax: SyntaxNodeRef<'a>) -> Option { + match syntax.kind() { + STRUCT_DEF => Some(ModuleItem::StructDef(StructDef { syntax })), + ENUM_DEF => Some(ModuleItem::EnumDef(EnumDef { syntax })), + FN_DEF => Some(ModuleItem::FnDef(FnDef { syntax })), + TRAIT_DEF => Some(ModuleItem::TraitDef(TraitDef { syntax })), + IMPL_ITEM => Some(ModuleItem::ImplItem(ImplItem { syntax })), + USE_ITEM => Some(ModuleItem::UseItem(UseItem { syntax })), + EXTERN_CRATE_ITEM => Some(ModuleItem::ExternCrateItem(ExternCrateItem { syntax })), + _ => None, + } + } + fn syntax(self) -> SyntaxNodeRef<'a> { + match self { + ModuleItem::StructDef(inner) => inner.syntax(), + ModuleItem::EnumDef(inner) => inner.syntax(), + ModuleItem::FnDef(inner) => inner.syntax(), + ModuleItem::TraitDef(inner) => inner.syntax(), + ModuleItem::ImplItem(inner) => inner.syntax(), + ModuleItem::UseItem(inner) => inner.syntax(), + ModuleItem::ExternCrateItem(inner) => inner.syntax(), + } + } +} + +impl<'a> ModuleItem<'a> {} + // Name #[derive(Debug, Clone, Copy)] pub struct Name<'a> { @@ -1762,6 +1820,24 @@ impl<'a> AstNode<'a> for TypeRef<'a> { impl<'a> TypeRef<'a> {} +// UseItem +#[derive(Debug, Clone, Copy)] +pub struct UseItem<'a> { + syntax: SyntaxNodeRef<'a>, +} + +impl<'a> AstNode<'a> for UseItem<'a> { + fn cast(syntax: SyntaxNodeRef<'a>) -> Option { + match syntax.kind() { + USE_ITEM => Some(UseItem { syntax }), + _ => None, + } + } + fn syntax(self) -> SyntaxNodeRef<'a> { self.syntax } +} + +impl<'a> UseItem<'a> {} + // WhereClause #[derive(Debug, Clone, Copy)] pub struct WhereClause<'a> { diff --git a/crates/libsyntax2/src/ast/mod.rs b/crates/libsyntax2/src/ast/mod.rs index 2ebee6a4f3..9941138a7a 100644 --- a/crates/libsyntax2/src/ast/mod.rs +++ b/crates/libsyntax2/src/ast/mod.rs @@ -115,6 +115,15 @@ impl<'a> Module<'a> { } } +impl<'a> LetStmt<'a> { + pub fn has_semi(self) -> bool { + match self.syntax().last_child() { + None => false, + Some(node) => node.kind() == SEMI, + } + } +} + impl<'a> IfExpr<'a> { pub fn then_branch(self) -> Option> { self.blocks().nth(0) diff --git a/crates/libsyntax2/src/grammar.ron b/crates/libsyntax2/src/grammar.ron index a98e9e2fdc..7217a46336 100644 --- a/crates/libsyntax2/src/grammar.ron +++ b/crates/libsyntax2/src/grammar.ron @@ -273,7 +273,7 @@ Grammar( "Module": ( traits: ["NameOwner", "AttrsOwner"], collections: [ - ["modules", "Module"] + ["items", "ModuleItem"] ] ), "ConstDef": ( traits: [ @@ -331,6 +331,10 @@ Grammar( "AttrsOwner" ], ), + "ModuleItem": ( + enum: ["StructDef", "EnumDef", "FnDef", "TraitDef", "ImplItem", + "UseItem", "ExternCrateItem" ] + ), "TupleExpr": (), "ArrayExpr": (), @@ -479,6 +483,8 @@ Grammar( ), "Param": ( options: [["pat", "Pat"]], - ) + ), + "UseItem": (), + "ExternCrateItem": (), }, ) diff --git a/crates/libsyntax2/src/grammar/expressions/atom.rs b/crates/libsyntax2/src/grammar/expressions/atom.rs index ab4aa49d2d..0769bb5a8b 100644 --- a/crates/libsyntax2/src/grammar/expressions/atom.rs +++ b/crates/libsyntax2/src/grammar/expressions/atom.rs @@ -33,6 +33,9 @@ pub(super) const ATOM_EXPR_FIRST: TokenSet = RETURN_KW, IDENT, SELF_KW, SUPER_KW, COLONCOLON, BREAK_KW, CONTINUE_KW, LIFETIME ], ]; +const EXPR_RECOVERY_SET: TokenSet = + token_set![LET_KW]; + pub(super) fn atom_expr(p: &mut Parser, r: Restrictions) -> Option { match literal(p) { Some(m) => return Some(m), @@ -73,7 +76,7 @@ pub(super) fn atom_expr(p: &mut Parser, r: Restrictions) -> Option continue_expr(p), BREAK_KW => break_expr(p), _ => { - p.err_and_bump("expected expression"); + p.err_recover("expected expression", EXPR_RECOVERY_SET); return None; } }; diff --git a/crates/libsyntax2/src/parser_api.rs b/crates/libsyntax2/src/parser_api.rs index 0a3b29b70f..10b9b64ac6 100644 --- a/crates/libsyntax2/src/parser_api.rs +++ b/crates/libsyntax2/src/parser_api.rs @@ -12,6 +12,8 @@ fn mask(kind: SyntaxKind) -> u128 { } impl TokenSet { + const EMPTY: TokenSet = TokenSet(0); + pub fn contains(&self, kind: SyntaxKind) -> bool { self.0 & mask(kind) != 0 } @@ -139,12 +141,21 @@ impl<'t> Parser<'t> { /// Create an error node and consume the next token. pub(crate) fn err_and_bump(&mut self, message: &str) { - let m = self.start(); - self.error(message); - if !self.at(SyntaxKind::L_CURLY) && !self.at(SyntaxKind::R_CURLY) { + self.err_recover(message, TokenSet::EMPTY); + } + + /// Create an error node and consume the next token. + pub(crate) fn err_recover(&mut self, message: &str, recovery_set: TokenSet) { + if self.at(SyntaxKind::L_CURLY) + || self.at(SyntaxKind::R_CURLY) + || recovery_set.contains(self.current()) { + self.error(message); + } else { + let m = self.start(); + self.error(message); self.bump(); + m.complete(self, ERROR); } - m.complete(self, ERROR); } } diff --git a/crates/libsyntax2/tests/data/parser/err/0017_incomplete_binexpr.txt b/crates/libsyntax2/tests/data/parser/err/0017_incomplete_binexpr.txt index db9a2f1751..f0be287ada 100644 --- a/crates/libsyntax2/tests/data/parser/err/0017_incomplete_binexpr.txt +++ b/crates/libsyntax2/tests/data/parser/err/0017_incomplete_binexpr.txt @@ -35,13 +35,12 @@ ROOT@[0; 47) INT_NUMBER@[33; 35) "92" SEMI@[35; 36) WHITESPACE@[36; 41) - BIN_EXPR@[41; 45) + BIN_EXPR@[41; 44) LITERAL@[41; 42) INT_NUMBER@[41; 42) "1" WHITESPACE@[42; 43) PLUS@[43; 44) - WHITESPACE@[44; 45) err: `expected expression` - ERROR@[45; 45) + WHITESPACE@[44; 45) R_CURLY@[45; 46) WHITESPACE@[46; 47) diff --git a/crates/libsyntax2/tests/data/parser/err/0018_incomplete_fn.txt b/crates/libsyntax2/tests/data/parser/err/0018_incomplete_fn.txt index 3f3a7784b0..58e39a3411 100644 --- a/crates/libsyntax2/tests/data/parser/err/0018_incomplete_fn.txt +++ b/crates/libsyntax2/tests/data/parser/err/0018_incomplete_fn.txt @@ -11,12 +11,12 @@ ROOT@[0; 183) ITEM_LIST@[14; 182) L_CURLY@[14; 15) WHITESPACE@[15; 20) - FN_DEF@[20; 181) + FN_DEF@[20; 180) FN_KW@[20; 22) WHITESPACE@[22; 23) NAME@[23; 32) IDENT@[23; 32) "new_scope" - PARAM_LIST@[32; 181) + PARAM_LIST@[32; 180) L_PAREN@[32; 33) PARAM@[33; 38) REF_PAT@[33; 35) @@ -163,17 +163,16 @@ ROOT@[0; 183) err: `expected parameters` err: `expected COMMA` WHITESPACE@[169; 170) - PARAM@[170; 181) + PARAM@[170; 180) BIND_PAT@[170; 180) NAME@[170; 180) IDENT@[170; 180) "set_parent" err: `expected COLON` - WHITESPACE@[180; 181) - err: `expected type` - err: `expected COMMA` - err: `expected value parameter` - err: `expected R_PAREN` - err: `expected a block` - ERROR@[181; 181) + err: `expected type` + err: `expected COMMA` + err: `expected value parameter` + err: `expected R_PAREN` + err: `expected a block` + WHITESPACE@[180; 181) R_CURLY@[181; 182) WHITESPACE@[182; 183) diff --git a/crates/libsyntax2/tests/data/parser/err/0019_let_recover.rs b/crates/libsyntax2/tests/data/parser/err/0019_let_recover.rs new file mode 100644 index 0000000000..52bddd494c --- /dev/null +++ b/crates/libsyntax2/tests/data/parser/err/0019_let_recover.rs @@ -0,0 +1,4 @@ +fn foo() { + let foo = + let bar = 1; +} diff --git a/crates/libsyntax2/tests/data/parser/err/0019_let_recover.txt b/crates/libsyntax2/tests/data/parser/err/0019_let_recover.txt new file mode 100644 index 0000000000..9ffa9d7818 --- /dev/null +++ b/crates/libsyntax2/tests/data/parser/err/0019_let_recover.txt @@ -0,0 +1,39 @@ +ROOT@[0; 44) + FN_DEF@[0; 43) + FN_KW@[0; 2) + WHITESPACE@[2; 3) + NAME@[3; 6) + IDENT@[3; 6) "foo" + PARAM_LIST@[6; 8) + L_PAREN@[6; 7) + R_PAREN@[7; 8) + WHITESPACE@[8; 9) + BLOCK@[9; 43) + L_CURLY@[9; 10) + WHITESPACE@[10; 15) + LET_STMT@[15; 24) + LET_KW@[15; 18) + WHITESPACE@[18; 19) + BIND_PAT@[19; 22) + NAME@[19; 22) + IDENT@[19; 22) "foo" + WHITESPACE@[22; 23) + EQ@[23; 24) + err: `expected expression` + err: `expected SEMI` + WHITESPACE@[24; 29) + LET_STMT@[29; 41) + LET_KW@[29; 32) + WHITESPACE@[32; 33) + BIND_PAT@[33; 36) + NAME@[33; 36) + IDENT@[33; 36) "bar" + WHITESPACE@[36; 37) + EQ@[37; 38) + WHITESPACE@[38; 39) + LITERAL@[39; 40) + INT_NUMBER@[39; 40) "1" + SEMI@[40; 41) + WHITESPACE@[41; 42) + R_CURLY@[42; 43) + WHITESPACE@[43; 44) diff --git a/crates/server/src/caps.rs b/crates/server/src/caps.rs index 98499bb05e..7456aea8a2 100644 --- a/crates/server/src/caps.rs +++ b/crates/server/src/caps.rs @@ -5,6 +5,7 @@ use languageserver_types::{ TextDocumentSyncKind, ExecuteCommandOptions, CompletionOptions, + DocumentOnTypeFormattingOptions, }; pub fn server_capabilities() -> ServerCapabilities { @@ -35,7 +36,10 @@ pub fn server_capabilities() -> ServerCapabilities { code_lens_provider: None, document_formatting_provider: None, document_range_formatting_provider: None, - document_on_type_formatting_provider: None, + document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { + first_trigger_character: "=".to_string(), + more_trigger_character: None, + }), rename_provider: None, color_provider: None, execute_command_provider: Some(ExecuteCommandOptions { diff --git a/crates/server/src/main_loop/handlers.rs b/crates/server/src/main_loop/handlers.rs index ec5421f06a..ca5cd5ab16 100644 --- a/crates/server/src/main_loop/handlers.rs +++ b/crates/server/src/main_loop/handlers.rs @@ -314,6 +314,25 @@ pub fn handle_completion( Ok(Some(req::CompletionResponse::Array(items))) } +pub fn handle_on_type_formatting( + world: ServerWorld, + params: req::DocumentOnTypeFormattingParams, +) -> Result>> { + if params.ch != "=" { + return Ok(None); + } + + let file_id = params.text_document.try_conv_with(&world)?; + let line_index = world.analysis().file_line_index(file_id)?; + let offset = params.position.conv_with(&line_index); + let file = world.analysis().file_syntax(file_id)?; + let action = match libeditor::on_eq_typed(&file, offset) { + None => return Ok(None), + Some(action) => action, + }; + Ok(Some(action.edit.conv_with(&line_index))) +} + pub fn handle_execute_command( world: ServerWorld, mut params: req::ExecuteCommandParams, diff --git a/crates/server/src/main_loop/mod.rs b/crates/server/src/main_loop/mod.rs index da1121cb44..accb138784 100644 --- a/crates/server/src/main_loop/mod.rs +++ b/crates/server/src/main_loop/mod.rs @@ -31,6 +31,7 @@ use { handle_completion, handle_runnables, handle_decorations, + handle_on_type_formatting, }, }; @@ -161,6 +162,9 @@ fn on_request( handle_request_on_threadpool::( &mut req, pool, world, sender, handle_decorations, )?; + handle_request_on_threadpool::( + &mut req, pool, world, sender, handle_on_type_formatting, + )?; dispatch::handle_request::(&mut req, |params, resp| { io.send(RawMsg::Response(resp.into_response(Ok(None))?)); diff --git a/crates/server/src/req.rs b/crates/server/src/req.rs index 999fdb7c23..881069b1f0 100644 --- a/crates/server/src/req.rs +++ b/crates/server/src/req.rs @@ -14,6 +14,7 @@ pub use languageserver_types::{ TextDocumentPositionParams, TextEdit, CompletionParams, CompletionResponse, + DocumentOnTypeFormattingParams, };