diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index 009501279109..a75e6be8b8ab 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -39,8 +39,11 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>{"; // Some features trigger on typing certain characters: // // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression +// - typing `=` between two expressions adds `;` when in statement position +// - typing `=` to turn an assignment into an equality comparison removes `;` when in expression position // - typing `.` in a chain method call auto-indents // - typing `{` in front of an expression inserts a closing `}` after the expression +// - typing `{` in a use item adds a closing `}` in the right place // // VS Code:: // @@ -166,11 +169,37 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option { 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; + + if let Some(edit) = let_stmt(file, offset) { + return Some(edit); } - if let Some(expr) = let_stmt.initializer() { + if let Some(edit) = assign_expr(file, offset) { + return Some(edit); + } + if let Some(edit) = assign_to_eq(file, offset) { + return Some(edit); + } + + return None; + + fn assign_expr(file: &SourceFile, offset: TextSize) -> Option { + let binop: ast::BinExpr = find_node_at_offset(file.syntax(), offset)?; + if !matches!(binop.op_kind(), Some(ast::BinaryOp::Assignment { op: None })) { + return None; + } + + // Parent must be `ExprStmt` or `StmtList` for `;` to be valid. + if let Some(expr_stmt) = ast::ExprStmt::cast(binop.syntax().parent()?) { + if expr_stmt.semicolon_token().is_some() { + return None; + } + } else { + if !ast::StmtList::can_cast(binop.syntax().parent()?.kind()) { + return None; + } + } + + let expr = binop.rhs()?; let expr_range = expr.syntax().text_range(); if expr_range.contains(offset) && offset != expr_range.start() { return None; @@ -178,11 +207,45 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option { if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { return None; } - } else { - return None; + let offset = expr.syntax().text_range().end(); + Some(TextEdit::insert(offset, ";".to_string())) + } + + /// `a =$0 b;` removes the semicolon if an expression is valid in this context. + fn assign_to_eq(file: &SourceFile, offset: TextSize) -> Option { + let binop: ast::BinExpr = find_node_at_offset(file.syntax(), offset)?; + if !matches!(binop.op_kind(), Some(ast::BinaryOp::CmpOp(ast::CmpOp::Eq { negated: false }))) + { + return None; + } + + let expr_stmt = ast::ExprStmt::cast(binop.syntax().parent()?)?; + let semi = expr_stmt.semicolon_token()?; + + if expr_stmt.syntax().next_sibling().is_some() { + // Not the last statement in the list. + return None; + } + + Some(TextEdit::delete(semi.text_range())) + } + + fn let_stmt(file: &SourceFile, offset: TextSize) -> Option { + let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; + if let_stmt.semicolon_token().is_some() { + return None; + } + let expr = let_stmt.initializer()?; + let expr_range = expr.syntax().text_range(); + if expr_range.contains(offset) && offset != expr_range.start() { + return None; + } + if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { + return None; + } + let offset = let_stmt.syntax().text_range().end(); + Some(TextEdit::insert(offset, ";".to_string())) } - let offset = let_stmt.syntax().text_range().end(); - Some(TextEdit::insert(offset, ";".to_string())) } /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. @@ -286,7 +349,7 @@ mod tests { } #[test] - fn test_on_eq_typed() { + fn test_semi_after_let() { // do_check(r" // fn foo() { // let foo =$0 @@ -322,6 +385,117 @@ fn foo() { // "); } + #[test] + fn test_semi_after_assign() { + type_char( + '=', + r#" +fn f() { + i $0 0 +} +"#, + r#" +fn f() { + i = 0; +} +"#, + ); + type_char( + '=', + r#" +fn f() { + i $0 0 + i +} +"#, + r#" +fn f() { + i = 0; + i +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(x: u8) { + if x $0 +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(x: u8) { + if x $0 {} +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(x: u8) { + if x $0 0 {} +} +"#, + ); + type_char_noop( + '=', + r#" +fn f() { + g(i $0 0); +} +"#, + ); + } + + #[test] + fn assign_to_eq() { + type_char( + '=', + r#" +fn f(a: u8) { + a =$0 0; +} +"#, + r#" +fn f(a: u8) { + a == 0 +} +"#, + ); + type_char( + '=', + r#" +fn f(a: u8) { + a $0= 0; +} +"#, + r#" +fn f(a: u8) { + a == 0 +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(a: u8) { + let e = a =$0 0; +} +"#, + ); + type_char_noop( + '=', + r#" +fn f(a: u8) { + let e = a =$0 0; + e +} +"#, + ); + } + #[test] fn indents_new_chain_call() { type_char(