diff --git a/crates/ide-assists/src/handlers/split_if.rs b/crates/ide-assists/src/handlers/split_if.rs new file mode 100644 index 000000000000..aca16781709b --- /dev/null +++ b/crates/ide-assists/src/handlers/split_if.rs @@ -0,0 +1,412 @@ +use syntax::{ + AstNode, SyntaxKind, SyntaxNode, T, + ast::{self, edit::AstNodeEdit, make}, + syntax_editor::{Position, SyntaxEditor}, +}; + +use crate::{AssistContext, AssistId, Assists}; + +// Assist: split_if +// +// Split if condition `&&` and `||`. +// +// ``` +// fn foo() { +// if a $0&& b && c { +// todo!() +// } +// } +// ``` +// -> +// ``` +// fn foo() { +// if a { +// if b && c { +// todo!() +// } +// } +// } +// ``` +// --- +// ``` +// fn foo() { +// if a $0|| b || c { +// todo!() +// } +// } +// ``` +// -> +// ``` +// fn foo() { +// if a { +// todo!() +// } else if b || c { +// todo!() +// } +// } +// ``` +pub(crate) fn split_if(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let expr = ctx.find_node_at_offset::()?; + let oper = expr.op_token()?; + let if_ = expr.syntax().ancestors().find_map(ast::IfExpr::cast)?; + let cond = if_.condition()?; + let block = if_.then_branch()?; + let (lhs, rhs) = expr.lhs().zip(expr.rhs())?; + + if oper.kind() != T![&&] && oper.kind() != T![||] { + return None; + } + if !is_subtree(expr.syntax(), cond.syntax()) { + return None; + } + if !oper.text_range().contains(ctx.offset()) { + return None; + } + + let new_if = if_.clone_subtree(); + let mut if_edit = SyntaxEditor::new(new_if.syntax().clone()); + let new_if_offset = if_.syntax().text_range().start(); + let new_cond = new_if.condition()?.syntax().clone(); + let new_expr = new_if.syntax().covering_element(expr.syntax().text_range() - new_if_offset); + + acc.add( + AssistId::refactor_rewrite("split_if"), + "Split if", + expr.syntax().text_range(), + |builder| { + let mut edit = builder.make_editor(if_.syntax()); + + if oper.kind() == T![||] { + if_edit.replace(new_cond, lhs.syntax().clone_for_update()); + let _ = remove_else_branch(&new_if, &mut if_edit); + edit.insert_all( + Position::before(if_.syntax()), + vec![ + if_edit.finish().new_root().clone().into(), + make::tokens::single_space().into(), + make::token(T![else]).into(), + make::tokens::single_space().into(), + ], + ); + edit.replace(expr.syntax(), rhs.syntax()); + } else { + if_edit.replace(new_expr, rhs.syntax().clone_for_update()); + let new_if = if_edit.finish().new_root().clone(); + if let Some(new_if) = ast::Expr::cast(new_if) { + let new_block = make::block_expr(None, Some(new_if)); + edit.replace(block.syntax(), new_block.indent(1.into()).syntax()); + } + edit.replace(cond.syntax(), lhs.syntax()); + } + + builder.add_file_edits(ctx.vfs_file_id(), edit); + }, + ) +} + +fn remove_else_branch(new_if: &ast::IfExpr, if_edit: &mut SyntaxEditor) -> Option<()> { + let else_token = new_if.else_token()?; + + if_edit.delete(&else_token); + + if else_token.prev_token()?.kind() == SyntaxKind::WHITESPACE { + if_edit.delete(else_token.prev_token()?); + } + if else_token.next_token()?.kind() == SyntaxKind::WHITESPACE { + if_edit.delete(else_token.next_token()?); + } + if_edit.delete(new_if.else_branch()?.syntax()); + + Some(()) +} + +fn is_subtree(node: &SyntaxNode, root: &SyntaxNode) -> bool { + node.ancestors().any(|it| it == *root) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn test_split_if_and() { + check_assist( + split_if, + r#" +fn foo() { + if a $0&& b && c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + r#" +fn foo() { + if a { + if b && c { + let _x = [ + 1, + 2, + ]; + } + } +} + "#, + ); + } + + #[test] + fn test_split_if_and_with_else_branch() { + check_assist( + split_if, + r#" +fn foo() { + if a $0&& b && c { + let _x = [ + 1, + 2, + ]; + } else { + foo() + } +} + "#, + r#" +fn foo() { + if a { + if b && c { + let _x = [ + 1, + 2, + ]; + } else { + foo() + } + } else { + foo() + } +} + "#, + ); + } + + #[test] + fn test_split_if_and_in_else_branch() { + check_assist( + split_if, + r#" +fn foo() { + if x { + todo!() + } else if a $0&& b && c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + r#" +fn foo() { + if x { + todo!() + } else if a { + if b && c { + let _x = [ + 1, + 2, + ]; + } + } +} + "#, + ); + } + + #[test] + fn test_split_if_or() { + check_assist( + split_if, + r#" +fn foo() { + if a $0|| b || c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + r#" +fn foo() { + if a { + let _x = [ + 1, + 2, + ]; + } else if b || c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + ); + + check_assist( + split_if, + r#" +fn foo() { + if a || b $0|| c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + r#" +fn foo() { + if a || b { + let _x = [ + 1, + 2, + ]; + } else if c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + ); + } + + #[test] + fn test_split_if_or_with_else_branch() { + check_assist( + split_if, + r#" +fn foo() { + if a $0|| b || c { + let _x = [ + 1, + 2, + ]; + } else { + foo() + } +} + "#, + r#" +fn foo() { + if a { + let _x = [ + 1, + 2, + ]; + } else if b || c { + let _x = [ + 1, + 2, + ]; + } else { + foo() + } +} + "#, + ); + + check_assist( + split_if, + r#" +fn foo() { + if a $0|| b || c { + let _x = [ + 1, + 2, + ]; + }else{ + foo() + } +} + "#, + r#" +fn foo() { + if a { + let _x = [ + 1, + 2, + ]; + } else if b || c { + let _x = [ + 1, + 2, + ]; + }else{ + foo() + } +} + "#, + ); + } + + #[test] + fn test_split_if_or_in_else_branch() { + check_assist( + split_if, + r#" +fn foo() { + if x { + todo!() + } else if a $0|| b || c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + r#" +fn foo() { + if x { + todo!() + } else if a { + let _x = [ + 1, + 2, + ]; + } else if b || c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + ); + } + + #[test] + fn test_split_if_not_applicable_without_operator() { + check_assist_not_applicable( + split_if, + r#" +fn foo() { + if $0a || b || c { + let _x = [ + 1, + 2, + ]; + } +} + "#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 4682c0473238..1d122ad809c5 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -218,6 +218,7 @@ mod handlers { mod replace_string_with_char; mod replace_turbofish_with_explicit_type; mod sort_items; + mod split_if; mod split_import; mod term_search; mod toggle_async_sugar; @@ -360,6 +361,7 @@ mod handlers { replace_qualified_name_with_use::replace_qualified_name_with_use, replace_turbofish_with_explicit_type::replace_turbofish_with_explicit_type, sort_items::sort_items, + split_if::split_if, split_import::split_import, term_search::term_search, toggle_async_sugar::desugar_async_into_impl_future, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 91348be97eb7..bb6474c7a20a 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -3338,6 +3338,52 @@ enum Animal { ) } +#[test] +fn doctest_split_if() { + check_doc_test( + "split_if", + r#####" +fn foo() { + if a $0&& b && c { + todo!() + } +} +"#####, + r#####" +fn foo() { + if a { + if b && c { + todo!() + } + } +} +"#####, + ) +} + +#[test] +fn doctest_split_if_1() { + check_doc_test( + "split_if", + r#####" +fn foo() { + if a $0|| b || c { + todo!() + } +} +"#####, + r#####" +fn foo() { + if a { + todo!() + } else if b || c { + todo!() + } +} +"#####, + ) +} + #[test] fn doctest_split_import() { check_doc_test( diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index 9897fd094157..c682bf7dc43e 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs @@ -1352,7 +1352,7 @@ pub mod tokens { pub(super) static SOURCE_FILE: LazyLock> = LazyLock::new(|| { SourceFile::parse( - "use crate::foo; const C: <()>::Item = ( true && true , true || true , 1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p, async { let _ @ [] })\n;\n\nunsafe impl A for B where: {}", + "use crate::foo; const C: <()>::Item = ( true && true , true || true , 1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p, async { let _ @ [] }, if false { false } else { true })\n;\n\nunsafe impl A for B where: {}", Edition::CURRENT, ) });