diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea6598ef20..34fbd8ba67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ - Fixed some quirk with the formatting of binary operators. - Fixed a bug where the formatter would move a function call's closed parentheses on a new line instead of splitting the function's arguments. +- Now the formatter will format tuples as if they were functions, trying to + first split just the last element before splitting the whole tuple. ### Build tool diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs index cabb72964cb..bf83fb979c2 100644 --- a/compiler-core/src/ast.rs +++ b/compiler-core/src/ast.rs @@ -821,6 +821,12 @@ impl CallArg { } } +impl HasLocation for CallArg { + fn location(&self) -> SrcSpan { + self.location + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct RecordUpdateSpread { pub base: Box, diff --git a/compiler-core/src/format.rs b/compiler-core/src/format.rs index 3b68e797697..8fea98e444a 100644 --- a/compiler-core/src/format.rs +++ b/compiler-core/src/format.rs @@ -704,10 +704,7 @@ impl<'comments> Formatter<'comments> { .append(".") .append(label.as_str()), - UntypedExpr::Tuple { elems, .. } => "#" - .to_doc() - .append(wrap_args(elems.iter().map(|e| self.expr(e)))) - .group(), + UntypedExpr::Tuple { elems, .. } => self.tuple(elems), UntypedExpr::BitArray { segments, .. } => bit_array( segments @@ -845,7 +842,11 @@ impl<'comments> Formatter<'comments> { } } - fn call<'a>(&mut self, fun: &'a UntypedExpr, args: &'a [CallArg]) -> Document<'a> { + fn call<'a>( + &mut self, + fun: &'a UntypedExpr, + args: &'a Vec>, + ) -> Document<'a> { let expr = match fun { UntypedExpr::Placeholder { .. } => panic!("Placeholders should not be formatted"), @@ -872,29 +873,57 @@ impl<'comments> Formatter<'comments> { | UntypedExpr::NegateInt { .. } => self.expr(fun), }; - match init_and_last(args) { - Some((initial_args, last_arg)) - if is_breakable_argument(&last_arg.value, args.len()) - && !self.any_comments(last_arg.location.start) => + self.append_inlinable_wrapped_args( + expr, + args, + |arg| &arg.value, + |self_, arg| self_.call_arg(arg), + ) + } + + fn tuple<'a>(&mut self, elements: &'a Vec) -> Document<'a> { + self.append_inlinable_wrapped_args("#".to_doc(), elements, |e| e, |self_, e| self_.expr(e)) + } + + // Appends to the given docs a comma-separated list of documents wrapped by + // parentheses. If the last item of the argument list is splittable the + // resulting document will try to first split that before splitting all the + // other arguments. + // This is used for function calls and tuples. + fn append_inlinable_wrapped_args<'a, T, ToExpr, ToDoc>( + &mut self, + doc: Document<'a>, + values: &'a Vec, + to_expr: ToExpr, + to_doc: ToDoc, + ) -> Document<'a> + where + T: HasLocation, + ToExpr: Fn(&T) -> &UntypedExpr, + ToDoc: Fn(&mut Self, &'a T) -> Document<'a>, + { + match init_and_last(values) { + Some((initial_values, last_value)) + if is_breakable_argument(to_expr(last_value), values.len()) + && !self.any_comments(last_value.location().start) => { - let last_arg_doc = self - .call_arg(last_arg) + let last_value_doc = to_doc(self, last_value) .group() .next_break_fits(NextBreakFitsMode::Enabled); - expr.append(wrap_function_call_args( - initial_args + doc.append(wrap_function_call_args( + initial_values .iter() - .map(|a| self.call_arg(a)) - .chain(std::iter::once(last_arg_doc)), + .map(|value| to_doc(self, value)) + .chain(std::iter::once(last_value_doc)), )) .next_break_fits(NextBreakFitsMode::Disabled) .group() } - Some(_) | None => expr + Some(_) | None => doc .append(wrap_function_call_args( - args.iter().map(|a| self.call_arg(a)), + values.iter().map(|value| to_doc(self, value)), )) .group(), } diff --git a/compiler-core/src/format/tests.rs b/compiler-core/src/format/tests.rs index 9f5fa574ad0..fc3d91f5aa0 100644 --- a/compiler-core/src/format/tests.rs +++ b/compiler-core/src/format/tests.rs @@ -643,13 +643,10 @@ fn compact_single_argument_call() { fn expr_tuple() { assert_format!( r#"fn main(one, two, three) { - #( - 1, - { - 1 - 2 - }, - ) + #(1, { + 1 + 2 + }) } "# ); diff --git a/compiler-core/src/format/tests/function.rs b/compiler-core/src/format/tests/function.rs index 6bfb02df037..e2c19a0776d 100644 --- a/compiler-core/src/format/tests/function.rs +++ b/compiler-core/src/format/tests/function.rs @@ -183,6 +183,7 @@ fn nested_breakable_tuples_in_function_calls() { html(#(attribute("lang", "en")), #( head(#(attribute("foo", "bar")), #( title(#(), #(text("Hello this is some HTML"))), + body(#(), #(text("Hello this is some HTML"))), )), body(#(), #(h1(#(), #(text("Hello, lisp!"))))), )) diff --git a/compiler-core/src/format/tests/tuple.rs b/compiler-core/src/format/tests/tuple.rs index 0e00b60ae06..fbd6243efed 100644 --- a/compiler-core/src/format/tests/tuple.rs +++ b/compiler-core/src/format/tests/tuple.rs @@ -24,3 +24,30 @@ fn index_block() { "# ); } + +#[test] +fn tuple_with_last_splittable_arg() { + assert_format!( + r#"fn on_attribute_change() -> Dict(String, Decoder(Msg)) { + dict.from_list([ + #("value", fn(attr) { + attr + |> dynamic.int + |> result.map(Value) + |> result.map(AttributeChanged) + }), + ]) +} +"# + ); + + assert_format!( + r#"pub fn main() { + #("value", [ + "a long list that needs to be split on multiple lines", + "another long string", + ]) +} +"# + ); +}