diff --git a/crates/ra_assists/src/assists/apply_demorgan.rs b/crates/ra_assists/src/assists/apply_demorgan.rs new file mode 100644 index 0000000000..5f2b0dd189 --- /dev/null +++ b/crates/ra_assists/src/assists/apply_demorgan.rs @@ -0,0 +1,102 @@ +//! This contains the functions associated with the demorgan assist. +//! This assist transforms boolean expressions of the form `!a || !b` into +//! `!(a && b)`. +use hir::db::HirDatabase; +use ra_syntax::ast::{self, AstNode}; +use ra_syntax::SyntaxNode; + +use crate::{Assist, AssistCtx, AssistId}; + +/// Assist for applying demorgan's law +/// +/// This transforms expressions of the form `!l || !r` into `!(l && r)`. +/// This also works with `&&`. This assist can only be applied with the cursor +/// on either `||` or `&&`, with both operands being a negation of some kind. +/// This means something of the form `!x` or `x != y`. +pub(crate) fn apply_demorgan(mut ctx: AssistCtx) -> Option { + let expr = ctx.node_at_offset::()?; + let op = expr.op_kind()?; + let op_range = expr.op_token()?.text_range(); + let opposite_op = opposite_logic_op(op)?; + let cursor_in_range = ctx.frange.range.is_subrange(&op_range); + if !cursor_in_range { + return None; + } + let lhs = expr.lhs()?.syntax().clone(); + let lhs_range = lhs.text_range(); + let rhs = expr.rhs()?.syntax().clone(); + let rhs_range = rhs.text_range(); + let not_lhs = undo_negation(lhs)?; + let not_rhs = undo_negation(rhs)?; + + ctx.add_action(AssistId("apply_demorgan"), "apply demorgan's law", |edit| { + edit.target(op_range); + edit.replace(op_range, opposite_op); + edit.replace(lhs_range, format!("!({}", not_lhs)); + edit.replace(rhs_range, format!("{})", not_rhs)); + }); + ctx.build() +} + +// Return the opposite text for a given logical operator, if it makes sense +fn opposite_logic_op(kind: ast::BinOp) -> Option<&'static str> { + match kind { + ast::BinOp::BooleanOr => Some("&&"), + ast::BinOp::BooleanAnd => Some("||"), + _ => None, + } +} + +// This function tries to undo unary negation, or inequality +fn undo_negation(node: SyntaxNode) -> Option { + match ast::Expr::cast(node)? { + ast::Expr::BinExpr(bin) => match bin.op_kind()? { + ast::BinOp::NegatedEqualityTest => { + let lhs = bin.lhs()?.syntax().text(); + let rhs = bin.rhs()?.syntax().text(); + Some(format!("{} == {}", lhs, rhs)) + } + _ => None, + }, + ast::Expr::PrefixExpr(pe) => match pe.op_kind()? { + ast::PrefixOp::Not => { + let child = pe.expr()?.syntax().text(); + Some(String::from(child)) + } + _ => None, + }, + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn demorgan_turns_and_into_or() { + check_assist(apply_demorgan, "fn f() { !x &&<|> !x }", "fn f() { !(x ||<|> x) }") + } + + #[test] + fn demorgan_turns_or_into_and() { + check_assist(apply_demorgan, "fn f() { !x ||<|> !x }", "fn f() { !(x &&<|> x) }") + } + + #[test] + fn demorgan_removes_inequality() { + check_assist(apply_demorgan, "fn f() { x != x ||<|> !x }", "fn f() { !(x == x &&<|> x) }") + } + + #[test] + fn demorgan_doesnt_apply_with_cursor_not_on_op() { + check_assist_not_applicable(apply_demorgan, "fn f() { <|> !x || !x }") + } + + #[test] + fn demorgan_doesnt_apply_when_operands_arent_negated_already() { + check_assist_not_applicable(apply_demorgan, "fn f() { x ||<|> x }") + } +} diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 91b2a1dcef..d2376c475c 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -92,6 +92,7 @@ mod assists { mod add_derive; mod add_explicit_type; mod add_impl; + mod apply_demorgan; mod flip_comma; mod flip_binexpr; mod change_visibility; @@ -113,6 +114,7 @@ mod assists { add_derive::add_derive, add_explicit_type::add_explicit_type, add_impl::add_impl, + apply_demorgan::apply_demorgan, change_visibility::change_visibility, fill_match_arms::fill_match_arms, merge_match_arms::merge_match_arms, diff --git a/docs/user/features.md b/docs/user/features.md index eb81cba263..0ce8f577b3 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -166,6 +166,20 @@ impl Foo for S { } ``` +- Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws) + +```rust +// before: +fn example(x: bool) -> bool { + !x || !x +} + +// after: +fn example(x: bool) -> bool { + !(x && x) +} +``` + - Import path ```rust