diff --git a/CHANGELOG.md b/CHANGELOG.md index 638b38b925b..03f71e4d876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ from 1.13 has been extended to int segments! Aside from the various performance improvements, this allows the compiler to mark more branches as unreachable. + ```gleam case bits { <<"a">> -> 0 @@ -111,6 +112,7 @@ _ -> 99 } ``` + ([fruno](https://github.com/fruno-bulax/)) ### Build tool @@ -178,6 +180,31 @@ ### Language server +- The language server can now offer a code action to merge consecutive case + branches with the same body. For example: + + ```gleam + case user { + Admin(name:, ..) -> todo + //^^^^^^^^^^^^^^^^^^^^^^^^ + Guest(name:, ..) -> todo + //^^^^^^^^^^^^^^^^ Selecting these two branches you can + // trigger the "Merge case branches" code action + _ -> todo + } + ``` + + Triggering the code action would result in the following code: + + ```gleam + case user { + Admin(name:, ..) | Guest(name:, ..) -> todo + _ -> todo + } + ``` + + ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) + - The "inline variable" code action can now trigger when used over the let keyword of a variable to inline. ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs index f3d9b42ab98..39028ab0b86 100644 --- a/compiler-core/src/ast.rs +++ b/compiler-core/src/ast.rs @@ -12,6 +12,7 @@ pub use self::untyped::{FunctionLiteralKind, UntypedExpr}; pub use self::constant::{Constant, TypedConstant, UntypedConstant}; use crate::analyse::Inferred; +use crate::ast::typed::pairwise_all; use crate::bit_array; use crate::build::{ExpressionPosition, Located, Target, module_erlang_name}; use crate::exhaustiveness::CompiledCase; @@ -1686,6 +1687,38 @@ impl TypedClause { .flatten() .flat_map(|pattern| pattern.bound_variables()) } + + fn syntactically_eq(&self, other: &Self) -> bool { + //pub pattern: MultiPattern, + //pub alternative_patterns: Vec>, + //pub guard: Option>, + //pub then: Expr, + // + let patterns_are_equal = pairwise_all(&self.pattern, &other.pattern, |(one, other)| { + one.syntactically_eq(other) + }); + + let alternatives_are_equal = pairwise_all( + &self.alternative_patterns, + &other.alternative_patterns, + |(patterns_one, patterns_other)| { + pairwise_all(patterns_one, patterns_other, |(one, other)| -> bool { + one.syntactically_eq(other) + }) + }, + ); + + let guards_are_equal = match (&self.guard, &other.guard) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(one), Some(other)) => one.syntactically_eq(other), + }; + + patterns_are_equal + && alternatives_are_equal + && guards_are_equal + && self.then.syntactically_eq(&other.then) + } } /// Returns true if a pattern and an expression are the same: that is the expression @@ -2284,6 +2317,286 @@ impl TypedClauseGuard { .union(right.referenced_variables()), } } + + fn syntactically_eq(&self, other: &Self) -> bool { + match (self, other) { + ( + ClauseGuard::Block { value, .. }, + ClauseGuard::Block { + value: other_value, .. + }, + ) => value.syntactically_eq(other_value), + (ClauseGuard::Block { .. }, _) => false, + + ( + ClauseGuard::Equals { left, right, .. }, + ClauseGuard::Equals { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::Equals { .. }, _) => false, + + ( + ClauseGuard::NotEquals { left, right, .. }, + ClauseGuard::NotEquals { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::NotEquals { .. }, _) => false, + + ( + ClauseGuard::GtInt { left, right, .. }, + ClauseGuard::GtInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::GtInt { .. }, _) => false, + + ( + ClauseGuard::GtEqInt { left, right, .. }, + ClauseGuard::GtEqInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::GtEqInt { .. }, _) => false, + + ( + ClauseGuard::LtInt { left, right, .. }, + ClauseGuard::LtInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::LtInt { .. }, _) => false, + + ( + ClauseGuard::LtEqInt { left, right, .. }, + ClauseGuard::LtEqInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::LtEqInt { .. }, _) => false, + + ( + ClauseGuard::GtFloat { left, right, .. }, + ClauseGuard::GtFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::GtFloat { .. }, _) => false, + + ( + ClauseGuard::GtEqFloat { left, right, .. }, + ClauseGuard::GtEqFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::GtEqFloat { .. }, _) => false, + + ( + ClauseGuard::LtFloat { left, right, .. }, + ClauseGuard::LtFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::LtFloat { .. }, _) => false, + + ( + ClauseGuard::LtEqFloat { left, right, .. }, + ClauseGuard::LtEqFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::LtEqFloat { .. }, _) => false, + + ( + ClauseGuard::AddInt { left, right, .. }, + ClauseGuard::AddInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::AddInt { .. }, _) => false, + + ( + ClauseGuard::AddFloat { left, right, .. }, + ClauseGuard::AddFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::AddFloat { .. }, _) => false, + + ( + ClauseGuard::SubInt { left, right, .. }, + ClauseGuard::SubInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::SubInt { .. }, _) => false, + + ( + ClauseGuard::SubFloat { left, right, .. }, + ClauseGuard::SubFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::SubFloat { .. }, _) => false, + + ( + ClauseGuard::MultInt { left, right, .. }, + ClauseGuard::MultInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::MultInt { .. }, _) => false, + + ( + ClauseGuard::MultFloat { left, right, .. }, + ClauseGuard::MultFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::MultFloat { .. }, _) => false, + + ( + ClauseGuard::DivInt { left, right, .. }, + ClauseGuard::DivInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::DivInt { .. }, _) => false, + + ( + ClauseGuard::DivFloat { left, right, .. }, + ClauseGuard::DivFloat { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::DivFloat { .. }, _) => false, + + ( + ClauseGuard::RemainderInt { left, right, .. }, + ClauseGuard::RemainderInt { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::RemainderInt { .. }, _) => false, + + ( + ClauseGuard::Or { left, right, .. }, + ClauseGuard::Or { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::Or { .. }, _) => false, + + ( + ClauseGuard::And { left, right, .. }, + ClauseGuard::And { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (ClauseGuard::And { .. }, _) => false, + + ( + ClauseGuard::Not { expression, .. }, + ClauseGuard::Not { + expression: other_expression, + .. + }, + ) => expression.syntactically_eq(other_expression), + (ClauseGuard::Not { .. }, _) => false, + + ( + ClauseGuard::Var { name, .. }, + ClauseGuard::Var { + name: other_name, .. + }, + ) => name == other_name, + (ClauseGuard::Var { .. }, _) => false, + + ( + ClauseGuard::TupleIndex { index, tuple, .. }, + ClauseGuard::TupleIndex { + index: other_index, + tuple: other_tuple, + .. + }, + ) => index == other_index && tuple.syntactically_eq(other_tuple), + (ClauseGuard::TupleIndex { .. }, _) => false, + + ( + ClauseGuard::FieldAccess { + label, container, .. + }, + ClauseGuard::FieldAccess { + label: other_label, + container: other_container, + .. + }, + ) => label == other_label && container.syntactically_eq(other_container), + (ClauseGuard::FieldAccess { .. }, _) => false, + + ( + ClauseGuard::ModuleSelect { + label, + module_alias, + .. + }, + ClauseGuard::ModuleSelect { + label: other_label, + module_alias: other_module_alias, + .. + }, + ) => label == other_label && module_alias == other_module_alias, + (ClauseGuard::ModuleSelect { .. }, _) => false, + + (ClauseGuard::Constant(one), ClauseGuard::Constant(other)) => { + one.syntactically_eq(other) + } + (ClauseGuard::Constant(_), _) => false, + } + } } #[derive( @@ -2520,6 +2833,51 @@ impl BitArraySize { BitArraySize::Variable { .. } | BitArraySize::BinaryOperator { .. } => false, } } + + fn syntactically_eq(&self, other: &Self) -> bool { + match (self, other) { + (BitArraySize::Int { int_value: n, .. }, BitArraySize::Int { int_value: m, .. }) => { + n == m + } + (BitArraySize::Int { .. }, _) => false, + + ( + BitArraySize::Variable { name, .. }, + BitArraySize::Variable { + name: other_name, .. + }, + ) => name == other_name, + (BitArraySize::Variable { .. }, _) => false, + + ( + BitArraySize::BinaryOperator { + operator, + left, + right, + .. + }, + BitArraySize::BinaryOperator { + operator: other_operator, + left: other_left, + right: other_right, + .. + }, + ) => { + operator == other_operator + && left.syntactically_eq(other_left) + && right.syntactically_eq(other_right) + } + (BitArraySize::BinaryOperator { .. }, _) => false, + + ( + BitArraySize::Block { inner, .. }, + BitArraySize::Block { + inner: other_inner, .. + }, + ) => inner.syntactically_eq(other_inner), + (BitArraySize::Block { .. }, _) => false, + } + } } pub type TypedTailPattern = TailPattern>; @@ -2621,6 +2979,163 @@ impl Pattern { } } +impl TypedPattern { + fn syntactically_eq(&self, other: &Self) -> bool { + match (self, other) { + (Pattern::Int { int_value: n, .. }, Pattern::Int { int_value: m, .. }) => n == m, + (Pattern::Int { .. }, _) => false, + + (Pattern::Float { float_value: n, .. }, Pattern::Float { float_value: m, .. }) => { + n == m + } + (Pattern::Float { .. }, _) => false, + + ( + Pattern::String { value, .. }, + Pattern::String { + value: other_value, .. + }, + ) => value == other_value, + (Pattern::String { .. }, _) => false, + + ( + Pattern::Variable { name, .. }, + Pattern::Variable { + name: other_name, .. + }, + ) => name == other_name, + (Pattern::Variable { .. }, _) => false, + + (Pattern::BitArraySize(one), Pattern::BitArraySize(other)) => { + one.syntactically_eq(other) + } + (Pattern::BitArraySize(..), _) => false, + + ( + Pattern::Assign { name, pattern, .. }, + Pattern::Assign { + name: other_name, + pattern: other_pattern, + .. + }, + ) => name == other_name && pattern.syntactically_eq(other_pattern), + (Pattern::Assign { .. }, _) => false, + + ( + Pattern::Discard { name, .. }, + Pattern::Discard { + name: other_name, .. + }, + ) => name == other_name, + (Pattern::Discard { .. }, _) => false, + + ( + Pattern::List { elements, tail, .. }, + Pattern::List { + elements: other_elements, + tail: other_tail, + .. + }, + ) => { + let tails_are_equal = match (tail, other_tail) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(one), Some(other)) => one.pattern.syntactically_eq(&other.pattern), + }; + tails_are_equal + && pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }) + } + (Pattern::List { .. }, _) => false, + + ( + Pattern::Constructor { + name, + arguments, + module, + .. + }, + Pattern::Constructor { + name: other_name, + arguments: other_arguments, + module: other_module, + .. + }, + ) => { + let modules_are_equal = match (module, other_module) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some((one, _)), Some((other, _))) => one == other, + }; + modules_are_equal + && name == other_name + && pairwise_all(arguments, other_arguments, |(one, other)| { + one.label == other.label && one.value.syntactically_eq(&other.value) + }) + } + (Pattern::Constructor { .. }, _) => false, + + ( + Pattern::Tuple { elements, .. }, + Pattern::Tuple { + elements: other_elements, + .. + }, + ) => pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }), + (Pattern::Tuple { .. }, _) => false, + + ( + Pattern::BitArray { segments, .. }, + Pattern::BitArray { + segments: other_segments, + .. + }, + ) => pairwise_all(segments, other_segments, |(one, other)| { + one.syntactically_eq(other) + }), + (Pattern::BitArray { .. }, _) => false, + + ( + Pattern::StringPrefix { + left_side_assignment, + left_side_string, + right_side_assignment, + .. + }, + Pattern::StringPrefix { + left_side_assignment: other_left_side_assignment, + left_side_string: other_left_side_string, + right_side_assignment: other_right_side_assignment, + .. + }, + ) => { + let left_side_assignments_are_equal = + match (left_side_assignment, other_left_side_assignment) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some((one, _)), Some((other, _))) => one == other, + }; + let right_side_assignments_are_equal = + match (right_side_assignment, other_right_side_assignment) { + (AssignName::Variable(one), AssignName::Variable(other)) => one == other, + (AssignName::Variable(_), AssignName::Discard(_)) => false, + (AssignName::Discard(one), AssignName::Discard(other)) => one == other, + (AssignName::Discard(_), AssignName::Variable(_)) => false, + }; + left_side_string == other_left_side_string + && left_side_assignments_are_equal + && right_side_assignments_are_equal + } + (Pattern::StringPrefix { .. }, _) => false, + + (Pattern::Invalid { .. }, _) => false, + } + } +} + /// A variable bound inside a pattern. #[derive(Debug, Clone)] pub enum BoundVariable { @@ -3129,6 +3644,15 @@ impl TypedExprBitArraySegment { pub fn find_node(&self, byte_index: u32) -> Option> { self.value.find_node(byte_index) } + + fn syntactically_eq(&self, other: &Self) -> bool { + self.value.syntactically_eq(&other.value) + && pairwise_all(&self.options, &other.options, |(option, other_option)| { + option.syntactically_eq(other_option, |size, other_size| { + size.syntactically_eq(other_size) + }) + }) + } } impl BitArraySegment> @@ -3214,6 +3738,15 @@ impl TypedPatternBitArraySegment { .find_map(|option| option.find_node(byte_index)) }) } + + fn syntactically_eq(&self, other: &Self) -> bool { + self.value.syntactically_eq(&other.value) + && pairwise_all(&self.options, &other.options, |(option, other_option)| { + option.syntactically_eq(other_option, |size, other_size| { + size.syntactically_eq(other_size) + }) + }) + } } impl TypedConstantBitArraySegment { @@ -3224,6 +3757,15 @@ impl TypedConstantBitArraySegment { .find_map(|option| option.find_node(byte_index)) }) } + + fn syntactically_eq(&self, other: &Self) -> bool { + self.value.syntactically_eq(&other.value) + && pairwise_all(&self.options, &other.options, |(option, other_option)| { + option.syntactically_eq(other_option, |size, other_size| { + size.syntactically_eq(other_size) + }) + }) + } } pub type TypedConstantBitArraySegmentOption = BitArrayOption; @@ -3376,6 +3918,75 @@ impl BitArrayOption { | BitArrayOption::Unit { .. } => false, } } + + fn syntactically_eq(&self, other: &Self, compare_sizes: impl Fn(&A, &A) -> bool) -> bool { + match (self, other) { + (BitArrayOption::Bytes { .. }, BitArrayOption::Bytes { .. }) => true, + (BitArrayOption::Bytes { .. }, _) => false, + + (BitArrayOption::Int { .. }, BitArrayOption::Int { .. }) => true, + (BitArrayOption::Int { .. }, _) => false, + + (BitArrayOption::Float { .. }, BitArrayOption::Float { .. }) => true, + (BitArrayOption::Float { .. }, _) => false, + + (BitArrayOption::Bits { .. }, BitArrayOption::Bits { .. }) => true, + (BitArrayOption::Bits { .. }, _) => false, + + (BitArrayOption::Utf8 { .. }, BitArrayOption::Utf8 { .. }) => true, + (BitArrayOption::Utf8 { .. }, _) => false, + + (BitArrayOption::Utf16 { .. }, BitArrayOption::Utf16 { .. }) => true, + (BitArrayOption::Utf16 { .. }, _) => false, + + (BitArrayOption::Utf32 { .. }, BitArrayOption::Utf32 { .. }) => true, + (BitArrayOption::Utf32 { .. }, _) => false, + + (BitArrayOption::Utf8Codepoint { .. }, BitArrayOption::Utf8Codepoint { .. }) => true, + (BitArrayOption::Utf8Codepoint { .. }, _) => false, + + (BitArrayOption::Utf16Codepoint { .. }, BitArrayOption::Utf16Codepoint { .. }) => true, + (BitArrayOption::Utf16Codepoint { .. }, _) => false, + + (BitArrayOption::Utf32Codepoint { .. }, BitArrayOption::Utf32Codepoint { .. }) => true, + (BitArrayOption::Utf32Codepoint { .. }, _) => false, + + (BitArrayOption::Signed { .. }, BitArrayOption::Signed { .. }) => true, + (BitArrayOption::Signed { .. }, _) => false, + + (BitArrayOption::Unsigned { .. }, BitArrayOption::Unsigned { .. }) => true, + (BitArrayOption::Unsigned { .. }, _) => false, + + (BitArrayOption::Big { .. }, BitArrayOption::Big { .. }) => true, + (BitArrayOption::Big { .. }, _) => false, + + (BitArrayOption::Little { .. }, BitArrayOption::Little { .. }) => true, + (BitArrayOption::Little { .. }, _) => false, + + (BitArrayOption::Native { .. }, BitArrayOption::Native { .. }) => true, + (BitArrayOption::Native { .. }, _) => false, + + ( + BitArrayOption::Unit { value, .. }, + BitArrayOption::Unit { + value: other_value, .. + }, + ) => value == other_value, + (BitArrayOption::Unit { .. }, _) => false, + + ( + BitArrayOption::Size { + value, short_form, .. + }, + BitArrayOption::Size { + value: other_value, + short_form: other_short_form, + .. + }, + ) => short_form == other_short_form && compare_sizes(value, other_value), + (BitArrayOption::Size { .. }, _) => false, + } + } } impl BitArrayOption { @@ -3768,6 +4379,34 @@ impl TypedStatement { Statement::Assert(_) => false, } } + + fn syntactically_eq(&self, other: &Self) -> bool { + match (self, other) { + (Statement::Expression(one), Statement::Expression(other)) => { + one.syntactically_eq(other) + } + (Statement::Expression(_), _) => false, + + (Statement::Assignment(one), Statement::Assignment(other)) => { + one.pattern.syntactically_eq(&other.pattern) + && one.value.syntactically_eq(&other.value) + } + (Statement::Assignment(_), _) => false, + + (Statement::Use(one), Statement::Use(other)) => one.call.syntactically_eq(&other.call), + (Statement::Use(_), _) => false, + + (Statement::Assert(one), Statement::Assert(other)) => { + let messages_are_equal = match (&one.message, &other.message) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(one), Some(other)) => one.syntactically_eq(other), + }; + messages_are_equal && one.value.syntactically_eq(&other.value) + } + (Statement::Assert(_), _) => false, + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/compiler-core/src/ast/constant.rs b/compiler-core/src/ast/constant.rs index 29dd259361c..901b6ca7293 100644 --- a/compiler-core/src/ast/constant.rs +++ b/compiler-core/src/ast/constant.rs @@ -177,6 +177,117 @@ impl TypedConstant { .union(right.referenced_variables()), } } + + pub(crate) fn syntactically_eq(&self, other: &Self) -> bool { + match (self, other) { + (Constant::Int { int_value: n, .. }, Constant::Int { int_value: m, .. }) => n == m, + (Constant::Int { .. }, _) => false, + + (Constant::Float { float_value: n, .. }, Constant::Float { float_value: m, .. }) => { + n == m + } + (Constant::Float { .. }, _) => false, + + ( + Constant::String { value, .. }, + Constant::String { + value: other_value, .. + }, + ) => value == other_value, + (Constant::String { .. }, _) => false, + + ( + Constant::Tuple { elements, .. }, + Constant::Tuple { + elements: other_elements, + .. + }, + ) => pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }), + (Constant::Tuple { .. }, _) => false, + + ( + Constant::List { elements, .. }, + Constant::List { + elements: other_elements, + .. + }, + ) => pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }), + (Constant::List { .. }, _) => false, + + ( + Constant::Record { + module, + name, + arguments, + .. + }, + Constant::Record { + module: other_module, + name: other_name, + arguments: other_arguments, + .. + }, + ) => { + let modules_are_equal = match (module, other_module) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some((one, _)), Some((other, _))) => one == other, + }; + + modules_are_equal + && name == other_name + && pairwise_all(arguments, other_arguments, |(one, other)| { + one.label == other.label && one.value.syntactically_eq(&other.value) + }) + } + (Constant::Record { .. }, _) => false, + + ( + Constant::BitArray { segments, .. }, + Constant::BitArray { + segments: other_segments, + .. + }, + ) => pairwise_all(segments, other_segments, |(one, other)| { + one.syntactically_eq(other) + }), + (Constant::BitArray { .. }, _) => false, + + ( + Constant::Var { module, name, .. }, + Constant::Var { + module: other_module, + name: other_name, + .. + }, + ) => { + let modules_are_equal = match (module, other_module) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some((one, _)), Some((other, _))) => one == other, + }; + + modules_are_equal && name == other_name + } + (Constant::Var { .. }, _) => false, + + ( + Constant::StringConcatenation { left, right, .. }, + Constant::StringConcatenation { + left: other_left, + right: other_right, + .. + }, + ) => left.syntactically_eq(other_left) && right.syntactically_eq(other_right), + (Constant::StringConcatenation { .. }, _) => false, + + (Constant::Invalid { .. }, _) => false, + } + } } impl HasType for TypedConstant { diff --git a/compiler-core/src/ast/typed.rs b/compiler-core/src/ast/typed.rs index 4d4bc7444b0..ae71d33d347 100644 --- a/compiler-core/src/ast/typed.rs +++ b/compiler-core/src/ast/typed.rs @@ -1136,6 +1136,321 @@ impl TypedExpr { _ => false, } } + + /// Checks that two expressions are written in the same (ignoring + /// whitespace). + /// + /// This is useful for the language server to know when it is possible to + /// merge two blocks of code together because they are the same. + /// Simply checking for equality of the AST nodes wouldn't work as those + /// also contain the source location (meaning that two expression that look + /// the same but are in different places would be considered different)! + /// + pub(crate) fn syntactically_eq(&self, other: &TypedExpr) -> bool { + match (self, other) { + (TypedExpr::Int { int_value: n, .. }, TypedExpr::Int { int_value: m, .. }) => n == m, + (TypedExpr::Int { .. }, _) => false, + + (TypedExpr::Float { float_value: n, .. }, TypedExpr::Float { float_value: m, .. }) => { + n == m + } + (TypedExpr::Float { .. }, _) => false, + + (TypedExpr::String { value, .. }, TypedExpr::String { value: other, .. }) => { + value == other + } + (TypedExpr::String { .. }, _) => false, + + ( + TypedExpr::Block { statements, .. }, + TypedExpr::Block { + statements: other, .. + }, + ) => pairwise_all(statements, other, |(one, other)| { + one.syntactically_eq(other) + }), + + (TypedExpr::Block { .. }, _) => false, + + ( + TypedExpr::List { elements, tail, .. }, + TypedExpr::List { + elements: other_elements, + tail: other_tail, + .. + }, + ) => { + let tails_are_equal = match (tail, other_tail) { + (Some(one), Some(other)) => one.syntactically_eq(other), + (None, Some(_)) | (Some(_), None) => false, + (None, None) => true, + }; + + tails_are_equal + && pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }) + } + (TypedExpr::List { .. }, _) => false, + + (TypedExpr::Var { name, .. }, TypedExpr::Var { name: other, .. }) => name == other, + (TypedExpr::Var { .. }, _) => false, + + ( + TypedExpr::Fn { + arguments, body, .. + }, + TypedExpr::Fn { + arguments: other_arguments, + body: other_body, + .. + }, + ) => { + let arguments_are_equal = + pairwise_all(arguments, other_arguments, |(one, other)| { + one.get_variable_name() == other.get_variable_name() + }); + + let bodies_are_equal = + pairwise_all(body, other_body, |(one, other)| one.syntactically_eq(other)); + + arguments_are_equal && bodies_are_equal + } + (TypedExpr::Fn { .. }, _) => false, + + ( + TypedExpr::Pipeline { + first_value, + assignments, + finally, + .. + }, + TypedExpr::Pipeline { + first_value: other_first_value, + assignments: other_assignments, + finally: other_finally, + .. + }, + ) => { + first_value.value.syntactically_eq(&other_first_value.value) + && pairwise_all(assignments, other_assignments, |(one, other)| { + one.0.value.syntactically_eq(&other.0.value) + }) + && finally.syntactically_eq(other_finally) + } + (TypedExpr::Pipeline { .. }, _) => false, + + ( + TypedExpr::Call { fun, arguments, .. }, + TypedExpr::Call { + fun: other_fun, + arguments: other_arguments, + .. + }, + ) => { + fun.syntactically_eq(other_fun) + && pairwise_all(arguments, other_arguments, |(one, other)| { + one.label == other.label && one.value.syntactically_eq(&other.value) + }) + } + (TypedExpr::Call { .. }, _) => false, + + ( + TypedExpr::BinOp { + name, left, right, .. + }, + TypedExpr::BinOp { + name: other_name, + left: other_left, + right: other_right, + .. + }, + ) => { + name == other_name + && left.syntactically_eq(other_left) + && right.syntactically_eq(other_right) + } + (TypedExpr::BinOp { .. }, _) => false, + + ( + TypedExpr::Case { + subjects, clauses, .. + }, + TypedExpr::Case { + subjects: other_subjects, + clauses: other_clauses, + .. + }, + ) => { + pairwise_all(subjects, other_subjects, |(one, other)| { + one.syntactically_eq(other) + }) && pairwise_all(clauses, other_clauses, |(one, other)| { + one.syntactically_eq(other) + }) + } + (TypedExpr::Case { .. }, _) => false, + + ( + TypedExpr::RecordAccess { label, record, .. }, + TypedExpr::RecordAccess { + label: other_label, + record: other_record, + .. + }, + ) => label == other_label && record.syntactically_eq(other_record), + (TypedExpr::RecordAccess { .. }, _) => false, + + ( + TypedExpr::ModuleSelect { + label, + module_alias, + .. + }, + TypedExpr::ModuleSelect { + label: other_label, + module_alias: other_module_alias, + .. + }, + ) => label == other_label && module_alias == other_module_alias, + (TypedExpr::ModuleSelect { .. }, _) => false, + + ( + TypedExpr::Tuple { elements, .. }, + TypedExpr::Tuple { + elements: other_elements, + .. + }, + ) => pairwise_all(elements, other_elements, |(one, other)| { + one.syntactically_eq(other) + }), + (TypedExpr::Tuple { .. }, _) => false, + + ( + TypedExpr::TupleIndex { index, tuple, .. }, + TypedExpr::TupleIndex { + index: other_index, + tuple: other_tuple, + .. + }, + ) => index == other_index && tuple.syntactically_eq(other_tuple), + (TypedExpr::TupleIndex { .. }, _) => false, + + ( + TypedExpr::Todo { message, kind, .. }, + TypedExpr::Todo { + message: other_message, + kind: other_kind, + .. + }, + ) => { + let messages_are_equal = match (message, other_message) { + (Some(one), Some(other)) => one.syntactically_eq(other), + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + }; + messages_are_equal && kind == other_kind + } + (TypedExpr::Todo { .. }, _) => false, + + ( + TypedExpr::Panic { message, .. }, + TypedExpr::Panic { + message: message_other, + .. + }, + ) => match (message, message_other) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(one), Some(other)) => one.syntactically_eq(other), + }, + (TypedExpr::Panic { .. }, _) => false, + + ( + TypedExpr::Echo { + expression, + message, + .. + }, + TypedExpr::Echo { + expression: other_expression, + message: other_message, + .. + }, + ) => { + let messages_are_equal = match (message, other_message) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(one), Some(other)) => one.syntactically_eq(other), + }; + let expressions_are_equal = match (expression, other_expression) { + (None, None) => true, + (None, Some(_)) | (Some(_), None) => false, + (Some(one), Some(other)) => one.syntactically_eq(other), + }; + messages_are_equal && expressions_are_equal + } + (TypedExpr::Echo { .. }, _) => false, + + ( + TypedExpr::BitArray { segments, .. }, + TypedExpr::BitArray { + segments: other_segments, + .. + }, + ) => pairwise_all(segments, other_segments, |(one, other)| { + one.syntactically_eq(other) + }), + (TypedExpr::BitArray { .. }, _) => false, + + ( + TypedExpr::RecordUpdate { + constructor, + arguments, + .. + }, + TypedExpr::RecordUpdate { + constructor: other_constructor, + arguments: other_arguments, + .. + }, + ) => { + constructor.syntactically_eq(other_constructor) + && pairwise_all(arguments, other_arguments, |(one, other)| { + one.label == other.label && one.value.syntactically_eq(&other.value) + }) + } + (TypedExpr::RecordUpdate { .. }, _) => false, + + ( + TypedExpr::NegateBool { value, .. }, + TypedExpr::NegateBool { + value: other_value, .. + }, + ) => value.syntactically_eq(other_value), + (TypedExpr::NegateBool { .. }, _) => false, + + (TypedExpr::NegateInt { value: n, .. }, TypedExpr::NegateInt { value: m, .. }) => { + n.syntactically_eq(m) + } + (TypedExpr::NegateInt { .. }, _) => false, + + (TypedExpr::Invalid { .. }, _) => false, + } + } + + pub(crate) fn is_todo_with_no_message(&self) -> bool { + match self { + TypedExpr::Todo { message: None, .. } => true, + _ => false, + } + } +} + +/// Checks that two slices have the same number of item and that the given +/// predicate holds for all pairs of items. +/// +pub(crate) fn pairwise_all(one: &[A], other: &[A], function: impl Fn((&A, &A)) -> bool) -> bool { + one.len() == other.len() && one.iter().zip(other).all(function) } fn is_non_zero_number(value: &EcoString) -> bool { diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 5de973f16bf..7b99d4402fe 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -9279,3 +9279,251 @@ impl<'ast> ast::visit::Visit<'ast> for ExtractFunction<'ast> { } } } + +/// Code action to merge two identical branches together. +/// +pub struct MergeCaseBranches<'a> { + module: &'a Module, + params: &'a CodeActionParams, + edits: TextEdits<'a>, + /// These are the positions of the patterns of all the consecutive branches + /// we've determined can be merged, for example if we're mergin the first + /// two branches here: + /// + /// ```gleam + /// case wibble { + /// 1 -> todo + /// // ^ this location here + /// 20 -> todo + /// // ^^ and this location here + /// _ -> todo + /// } + /// ``` + /// + /// We need those to delete all the space between each consecutive pattern, + /// replacing it with the `|` for alternatives + /// + patterns_to_merge: Option, +} + +struct MergeableBranches { + /// The span of the body to keep when merging multiple branches. For + /// example: + /// + /// ```gleam + /// case n { + /// // Imagine we're merging the first three branches together... + /// 1 -> todo + /// 2 -> n * 2 + /// // ^^^^^ This would be the location of the one body to keep + /// 3 -> todo + /// _ -> todo + /// } + /// ``` + /// + body_to_keep: SrcSpan, + + /// The location body of the last of the branches that are going to be + /// merged; that is where we're going to place the code of the body to keep + /// once the action is done. For example: + /// + /// ```gleam + /// case n { + /// // Imagine we're merging the first three branches together... + /// 1 -> todo + /// 2 -> n * 2 + /// 3 -> todo + /// // ^^^^ This would be the location of the final body + /// _ -> todo + /// } + /// ``` + /// + final_body: SrcSpan, + + /// The span of the patterns whose branches are going to be merged. For + /// example: + /// + /// ```gleam + /// case n { + /// // Imagine we're merging the first three branches together... + /// 1 -> todo + /// // ^ + /// 2 -> n * 2 + /// // ^ + /// 3 -> todo + /// // ^ These would be the locations of the patterns + /// _ -> todo + /// } + /// ``` + /// + patterns_to_merge: Vec, +} + +impl<'a> MergeCaseBranches<'a> { + pub fn new( + module: &'a Module, + line_numbers: &'a LineNumbers, + params: &'a CodeActionParams, + ) -> Self { + Self { + module, + params, + edits: TextEdits::new(line_numbers), + patterns_to_merge: None, + } + } + + pub fn code_actions(mut self) -> Vec { + self.visit_typed_module(&self.module.ast); + + let Some(mergeable_branches) = self.patterns_to_merge else { + return vec![]; + }; + + for (one, next) in mergeable_branches.patterns_to_merge.iter().tuple_windows() { + self.edits + .replace(SrcSpan::new(one.end, next.start), " | ".into()); + } + + self.edits.replace( + mergeable_branches.final_body, + code_at(self.module, mergeable_branches.body_to_keep).into(), + ); + + let mut action = Vec::with_capacity(1); + CodeActionBuilder::new("Merge case branches") + .kind(CodeActionKind::REFACTOR_REWRITE) + .changes(self.params.text_document.uri.clone(), self.edits.edits) + .preferred(false) + .push_to(&mut action); + action + } + + fn select_mergeable_branches( + &self, + clauses: &'a [ast::TypedClause], + ) -> Option { + let mut clauses = clauses + .iter() + // We want to skip all the branches at the beginning of the case + // expression that the cursor is not hovering over. For example: + // + // ```gleam + // case wibble { + // a -> 1 <- we want to skip this one here that is not selected + // b -> 2 + // ^^^^ this is the selection + // _ -> 3 + // ^^ + // } + // ``` + .skip_while(|clause| { + let clause_range = self.edits.src_span_to_lsp_range(clause.location); + !overlaps(self.params.range, clause_range) + }) + // Then we only want to take the clauses that we're hovering over + // with our selection (even partially!) + // In the provious example they would be `b -> 2` and `_ -> 3`. + .take_while(|clause| { + let clause_range = self.edits.src_span_to_lsp_range(clause.location); + overlaps(self.params.range, clause_range) + }); + + let first_hovered_clause = clauses.next()?; + + // This is the clause we're comparing all the others with. We need to + // make sure that all the clauses we're going to join can be merged with + // this one. + let mut reference_clause = first_hovered_clause; + let mut clause_patterns_to_merge = vec![reference_clause.pattern_location()]; + let mut final_body = first_hovered_clause.then.location(); + + for clause in clauses { + // As soon as we find a clause that can't be merged with the current + // reference we know we're done looking for consecutive clauses to + // merge. + if !clauses_can_be_merged(reference_clause, clause) { + break; + } + + clause_patterns_to_merge.push(clause.pattern_location()); + final_body = clause.then.location(); + + // If the current reference is a `todo` expression, we want to use + // the newly found mergeable clause as the next reference. The + // reference clause is the one whose body will be kept around, so if + // we can we avoid keeping `todo`s + if reference_clause.then.is_todo_with_no_message() { + reference_clause = clause; + } + } + + // We only offer the code action if we have found two or more clauses + // to merge. + if clause_patterns_to_merge.len() >= 2 { + Some(MergeableBranches { + final_body, + body_to_keep: reference_clause.then.location(), + patterns_to_merge: clause_patterns_to_merge, + }) + } else { + None + } + } +} + +fn clauses_can_be_merged(one: &ast::TypedClause, other: &ast::TypedClause) -> bool { + // Two clauses cannot be merged if any of those has an if guard + if one.guard.is_some() || other.guard.is_some() { + return false; + } + + // Two clauses can only be merged if they define the same variables, + // otherwise joining them would result in invalid code. + let variables_one = one + .bound_variables() + .map(|variable| variable.name()) + .collect::>(); + + let variables_other = other + .bound_variables() + .map(|variable| variable.name()) + .collect::>(); + + if variables_one != variables_other { + return false; + } + + // Anything can be merged with a simple todo, or the two bodies must be + // syntactically equal. + one.then.is_todo_with_no_message() + || other.then.is_todo_with_no_message() + || one.then.syntactically_eq(&other.then) +} + +impl<'ast> ast::visit::Visit<'ast> for MergeCaseBranches<'ast> { + fn visit_typed_expr_case( + &mut self, + location: &'ast SrcSpan, + type_: &'ast Arc, + subjects: &'ast [TypedExpr], + clauses: &'ast [ast::TypedClause], + compiled_case: &'ast CompiledCase, + ) { + // We only trigger the code action if we are within a case expression, + // otherwise there's no point in exploring the expression any further. + let case_range = self.edits.src_span_to_lsp_range(*location); + if !within(self.params.range, case_range) { + return; + } + + if let result @ Some(_) = self.select_mergeable_branches(clauses) { + self.patterns_to_merge = result + } + + // We still need to visit the case expression in case we want to apply + // the code action to some case expression that is nested in one of its + // branches! + ast::visit::visit_typed_expr_case(self, location, type_, subjects, clauses, compiled_case); + } +} diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index 7c47f91979c..d624a83d83f 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -13,7 +13,7 @@ use crate::{ io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter}, language_server::{ code_action::{ - AddOmittedLabels, CollapseNestedCase, ExtractFunction, RemoveBlock, + AddOmittedLabels, CollapseNestedCase, ExtractFunction, MergeCaseBranches, RemoveBlock, RemovePrivateOpaque, RemoveUnreachableCaseClauses, }, compiler::LspProjectCompiler, @@ -420,6 +420,7 @@ where &this.error, &mut actions, ); + actions.extend(MergeCaseBranches::new(module, &lines, ¶ms).code_actions()); actions.extend(FixBinaryOperation::new(module, &lines, ¶ms).code_actions()); actions .extend(FixTruncatedBitArraySegment::new(module, &lines, ¶ms).code_actions()); diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index 4e788cdab84..84b5c9c0dda 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -136,6 +136,7 @@ const COLLAPSE_NESTED_CASE: &str = "Collapse nested case"; const REMOVE_UNREACHABLE_CLAUSES: &str = "Remove unreachable clauses"; const ADD_OMITTED_LABELS: &str = "Add omitted labels"; const EXTRACT_FUNCTION: &str = "Extract function"; +const MERGE_CASE_BRANCHES: &str = "Merge case branches"; macro_rules! assert_code_action { ($title:expr, $code:literal, $range:expr $(,)?) => { @@ -10845,3 +10846,224 @@ fn wibble() -> Nil find_position_of("wibble").to_selection() ); } + +#[test] +fn merge_case_branch() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> todo + 2 -> todo + _ -> todo + } + }"#, + find_position_of("1").select_until(find_position_of("2")) + ); +} + +#[test] +fn merge_case_branch_with_todo_keeps_the_non_todo_body() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> todo + 2 -> n * 2 + 3 -> todo + _ -> todo + } + }"#, + find_position_of("1").select_until(find_position_of("3")) + ); +} + +#[test] +fn merge_case_branch_with_todo_keeps_the_non_todo_body_1() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> todo + 2 -> todo + 3 -> n * 2 + _ -> todo + } + }"#, + find_position_of("1").select_until(find_position_of("3")) + ); +} + +#[test] +fn merge_case_branch_with_todo_keeps_the_non_todo_body_2() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> n * 2 + 2 -> todo + 3 -> todo + _ -> todo + } + }"#, + find_position_of("1").select_until(find_position_of("3")) + ); +} + +#[test] +fn merge_case_branch_with_complex_bodies_1() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> Ok("one or two") + 2 -> Ok("one or two") + _ -> Error("neither one or two") + } + }"#, + find_position_of("1").select_until(find_position_of("2")) + ); +} + +#[test] +fn merge_case_branch_with_complex_bodies_2() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> n + 2 -> n + _ -> panic as "neither one nor two" + } + }"#, + find_position_of("1").select_until(find_position_of("2")) + ); +} + +#[test] +fn merge_case_branch_with_complex_bodies_3() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> go(n - 1) + 2 -> go(n - 1) + _ -> 10 + } +}"#, + find_position_of("1").select_until(find_position_of("2")) + ); +} + +#[test] +fn merge_case_branch_with_complex_bodies_4() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 -> { + let a = go(n - 1) + a * 10 + } + 2 -> { + let a = go(n - 1) + a * 10 + } + _ -> 10 + } +}"#, + find_position_of("1").select_until(find_position_of("2")) + ); +} + +#[test] +fn merge_case_branch_will_not_merge_branches_with_guards() { + assert_no_code_actions!( + MERGE_CASE_BRANCHES, + r#"pub fn go(n: Int) { + case n { + 1 if True -> todo + 2 -> todo + _ -> todo + } + }"#, + find_position_of("1").select_until(find_position_of("2")) + ); +} + +#[test] +fn merge_case_branch_will_not_merge_branches_defining_different_variables() { + assert_no_code_actions!( + MERGE_CASE_BRANCHES, + r#"pub fn go(result) { + case result { + Ok(value) -> todo + Error(error) -> todo + _ -> todo + } + }"#, + find_position_of("Ok").select_until(find_position_of("error")) + ); +} + +#[test] +fn merge_case_branch_can_merge_branches_defining_the_same_variables() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(result) { + case result { + [Ok(value), ..] -> todo + [_, Error(value)] -> todo + _ -> todo + } +}"#, + find_position_of("Ok").select_until(find_position_of("todo").nth_occurrence(2)) + ); +} + +#[test] +fn merge_case_branch_can_merge_multiple_branches() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(result) { + case result { + [_] -> 1 + [Ok(value), ..] -> todo + [_, Error(value)] -> todo + [_, _, Error(value)] -> todo + [_, _] -> 1 + _ -> 2 + } +}"#, + find_position_of("todo").select_until(find_position_of("todo").nth_occurrence(3)) + ); +} + +#[test] +fn merge_case_branch_does_not_pop_up_with_a_single_selected_branch() { + assert_no_code_actions!( + MERGE_CASE_BRANCHES, + r#"pub fn go(result) { + case result { + [] -> todo + _ -> 2 + } +}"#, + find_position_of("[]").to_selection() + ); +} + +#[test] +fn merge_case_branch_works_with_existing_alternative_patterns() { + assert_code_action!( + MERGE_CASE_BRANCHES, + r#"pub fn go(result) { + case result { + [] | [_, _, ..]-> todo + [_] -> todo + _ -> 2 + } +}"#, + find_position_of("[]").select_until(find_position_of("[_]")) + ); +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch.snap new file mode 100644 index 00000000000..cea4c134e39 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> todo\n 2 -> todo\n _ -> todo\n }\n }" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> todo + ▔▔▔▔▔▔▔▔▔ + 2 -> todo +▔▔▔▔↑ + _ -> todo + } + } + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 -> todo + _ -> todo + } + } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_can_merge_branches_defining_the_same_variables.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_can_merge_branches_defining_the_same_variables.snap new file mode 100644 index 00000000000..6e3fb6c2cbd --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_can_merge_branches_defining_the_same_variables.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(result) {\n case result {\n [Ok(value), ..] -> todo\n [_, Error(value)] -> todo\n _ -> todo\n }\n}" +--- +----- BEFORE ACTION +pub fn go(result) { + case result { + [Ok(value), ..] -> todo + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + [_, Error(value)] -> todo +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔↑ + _ -> todo + } +} + + +----- AFTER ACTION +pub fn go(result) { + case result { + [Ok(value), ..] | [_, Error(value)] -> todo + _ -> todo + } +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_can_merge_multiple_branches.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_can_merge_multiple_branches.snap new file mode 100644 index 00000000000..36a49d8edc1 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_can_merge_multiple_branches.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(result) {\n case result {\n [_] -> 1\n [Ok(value), ..] -> todo\n [_, Error(value)] -> todo\n [_, _, Error(value)] -> todo\n [_, _] -> 1\n _ -> 2\n }\n}" +--- +----- BEFORE ACTION +pub fn go(result) { + case result { + [_] -> 1 + [Ok(value), ..] -> todo + ▔▔▔▔ + [_, Error(value)] -> todo +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + [_, _, Error(value)] -> todo +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔↑ + [_, _] -> 1 + _ -> 2 + } +} + + +----- AFTER ACTION +pub fn go(result) { + case result { + [_] -> 1 + [Ok(value), ..] | [_, Error(value)] | [_, _, Error(value)] -> todo + [_, _] -> 1 + _ -> 2 + } +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_1.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_1.snap new file mode 100644 index 00000000000..200579c4485 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_1.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> Ok(\"one or two\")\n 2 -> Ok(\"one or two\")\n _ -> Error(\"neither one or two\")\n }\n }" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> Ok("one or two") + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 2 -> Ok("one or two") +▔▔▔▔↑ + _ -> Error("neither one or two") + } + } + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 -> Ok("one or two") + _ -> Error("neither one or two") + } + } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_2.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_2.snap new file mode 100644 index 00000000000..e2d0dd41e65 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_2.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> n\n 2 -> n\n _ -> panic as \"neither one nor two\"\n }\n }" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> n + ▔▔▔▔▔▔ + 2 -> n +▔▔▔▔↑ + _ -> panic as "neither one nor two" + } + } + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 -> n + _ -> panic as "neither one nor two" + } + } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_3.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_3.snap new file mode 100644 index 00000000000..1e03a09f726 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_3.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> go(n - 1)\n 2 -> go(n - 1)\n _ -> 10\n }\n}" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> go(n - 1) + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 2 -> go(n - 1) +▔▔▔▔↑ + _ -> 10 + } +} + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 -> go(n - 1) + _ -> 10 + } +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_4.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_4.snap new file mode 100644 index 00000000000..2764d558d27 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_complex_bodies_4.snap @@ -0,0 +1,35 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> {\n let a = go(n - 1)\n a * 10\n }\n 2 -> {\n let a = go(n - 1)\n a * 10\n }\n _ -> 10\n }\n}" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> { + ▔▔▔▔▔▔ + let a = go(n - 1) +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + a * 10 +▔▔▔▔▔▔▔▔▔▔▔▔ + } +▔▔▔▔▔ + 2 -> { +▔▔▔▔↑ + let a = go(n - 1) + a * 10 + } + _ -> 10 + } +} + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 -> { + let a = go(n - 1) + a * 10 + } + _ -> 10 + } +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body.snap new file mode 100644 index 00000000000..0a7c0ed9a0b --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> todo\n 2 -> n * 2\n 3 -> todo\n _ -> todo\n }\n }" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> todo + ▔▔▔▔▔▔▔▔▔ + 2 -> n * 2 +▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 3 -> todo +▔▔▔▔↑ + _ -> todo + } + } + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 | 3 -> n * 2 + _ -> todo + } + } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body_1.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body_1.snap new file mode 100644 index 00000000000..9c9c504d66a --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body_1.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> todo\n 2 -> todo\n 3 -> n * 2\n _ -> todo\n }\n }" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> todo + ▔▔▔▔▔▔▔▔▔ + 2 -> todo +▔▔▔▔▔▔▔▔▔▔▔▔▔ + 3 -> n * 2 +▔▔▔▔↑ + _ -> todo + } + } + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 | 3 -> n * 2 + _ -> todo + } + } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body_2.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body_2.snap new file mode 100644 index 00000000000..a66d6200b22 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_with_todo_keeps_the_non_todo_body_2.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(n: Int) {\n case n {\n 1 -> n * 2\n 2 -> todo\n 3 -> todo\n _ -> todo\n }\n }" +--- +----- BEFORE ACTION +pub fn go(n: Int) { + case n { + 1 -> n * 2 + ▔▔▔▔▔▔▔▔▔▔ + 2 -> todo +▔▔▔▔▔▔▔▔▔▔▔▔▔ + 3 -> todo +▔▔▔▔↑ + _ -> todo + } + } + + +----- AFTER ACTION +pub fn go(n: Int) { + case n { + 1 | 2 | 3 -> n * 2 + _ -> todo + } + } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_works_with_existing_alternative_patterns.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_works_with_existing_alternative_patterns.snap new file mode 100644 index 00000000000..a09bfa5ede9 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__merge_case_branch_works_with_existing_alternative_patterns.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn go(result) {\n case result {\n [] | [_, _, ..]-> todo\n [_] -> todo\n _ -> 2\n }\n}" +--- +----- BEFORE ACTION +pub fn go(result) { + case result { + [] | [_, _, ..]-> todo + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + [_] -> todo +▔▔▔▔↑ + _ -> 2 + } +} + + +----- AFTER ACTION +pub fn go(result) { + case result { + [] | [_, _, ..] | [_] -> todo + _ -> 2 + } +}