From a0a36c7993c854ff1a6f93d110ea743189ed3ade Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 19 Feb 2025 05:21:55 -0600 Subject: [PATCH] Fix handling of hugging calls (#228) --- CHANGELOG.md | 13 + .../src/r/auxiliary/call_arguments.rs | 692 ++++++++++-------- crates/air_r_formatter/tests/specs/r/call.R | 139 ++++ .../air_r_formatter/tests/specs/r/call.R.snap | 305 ++++++++ 4 files changed, 830 insertions(+), 319 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3544ff07..86a3cc76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ - Assigned pipelines no longer double-indent when a persistent line break is used (#220). +- Hugging calls like: + + ```r + list(c( + 1, + 2 + )) + ``` + + are no longer fully expanded (#21). + +- Assigned pipelines no longer double-indent (#220). + # 0.3.0 diff --git a/crates/air_r_formatter/src/r/auxiliary/call_arguments.rs b/crates/air_r_formatter/src/r/auxiliary/call_arguments.rs index b18d66a2..ef49c314 100644 --- a/crates/air_r_formatter/src/r/auxiliary/call_arguments.rs +++ b/crates/air_r_formatter/src/r/auxiliary/call_arguments.rs @@ -4,6 +4,7 @@ use std::cell::Cell; use crate::comments::RComments; use crate::context::RFormatOptions; +use crate::either::Either; use crate::prelude::*; use crate::r::auxiliary::braced_expressions::as_curly_curly; use crate::r::auxiliary::function_definition::FormatFunctionOptions; @@ -75,6 +76,326 @@ impl RCallLikeArguments { Self::Subset2(node) => node.syntax(), } } + + /// Writes the function arguments + /// + /// The "grouped" argument is either the first or last argument depending on the + /// `group_layout`, but currently it is always the last one. + /// + /// - If any arguments that aren't the grouped argument *force* a break, then we + /// print in fully expanded mode. + /// + /// - If the grouped argument is an inline function with `parameters` that would + /// *force* a break, then we print in fully expanded mode. We only want to + /// allow forced breaks in a braced expression body. + /// + /// If neither of those trigger fully expanded mode, we best-fit between three + /// possible forms: + /// + /// ## Most expanded + /// + /// The `(`, `)`, and all arguments are within a single `group()`, and that + /// group is marked with `should_expand(true)`. The arguments are wrapped in + /// `soft_block_indent()`, and each argument is separated by a + /// `soft_line_break_or_space()`. Due to the forced expansion, these all + /// become hard indents / line breaks, i.e. the "most expanded" form. + /// + /// Example: + /// + /// ```r + /// map( + /// xs, + /// function(x) { + /// x + 1 + /// } + /// ) + /// ``` + /// + /// ## Most flat + /// + /// Arguments are not grouped, each argument is separated by a + /// `soft_line_break_or_space()`, no forced expansion is done. + /// + /// Special formatting is done for a grouped argument that is an inline + /// function. We remove any soft line breaks in the `parameters`, which + /// practically means the only place it is allowed to break is in the function + /// body (but the break is not forced). + /// + /// Example: + /// + /// ```r + /// # NOTE: Not currently possible, as the `{}` always force a break right now, + /// # but this would be an example if `{}` didn't force a break. + /// map(xs, function(x) {}) + /// ``` + /// + /// This variant is removed from the set if we detect that the grouped argument + /// contains a forced break in the body (if a forced break is found in the + /// parameters, we bail entirely and use the most expanded form, as noted + /// at the beginning of this documentation page). + /// + /// Note that because `{}` currently unconditionally force a break, and because + /// we only go down this path when we have a `{}` to begin with, that means that + /// currently the most flat variant is always removed. There is an + /// `unreachable!()` in the code to assert this. We can't simply remove the + /// `most_flat` code path though, because it is also where we detect if a + /// parameter forces a break, triggering one of our early exists. Additionally, + /// in the future we may allow `{}` to not force a break, meaning this variant + /// may come back into play. + /// + /// ```r + /// # NOTE: We explicitly disallow curly-curly as a groupable argument, + /// # so this case is never considered grouped, and is therefore not an + /// # example of "most flat". + /// group_by(df, {{ by }}) + /// ``` + /// + /// ## Middle variant + /// + /// Exactly the same as "most flat", except that the grouped argument is put + /// in its own `group()` marked with `should_expand(true)`. The soft line breaks + /// are removed from any grouped argument parameters, like with most flat. + /// + /// Example: + /// + /// ```r + /// map(xs, function(x) { + /// x + 1 + /// }) + /// ``` + /// + /// ```r + /// # The soft line breaks are removed from the `parameters`, meaning that this... + /// map(xs, function(x, a_long_secondary_argument = "with a default", and_another_one_here) { + /// x + 1 + /// }) + /// + /// # ...is not allowed to be formatted as... + /// map(xs, function( + /// x, + /// a_long_secondary_argument = "with a default", + /// and_another_one_here + /// ) { + /// x + 1 + /// }) + /// + /// # ...and instead the most expanded form is chosen by best-fitting: + /// map( + /// xs, + /// function( + /// x, + /// a_long_secondary_argument = "with a default", + /// and_another_one_here + /// ) { + /// x + 1 + /// } + /// ) + /// ``` + fn write_grouped_arguments( + &self, + l_token: RSyntaxToken, + leading_holes: Vec, + mut arguments: Vec, + r_token: RSyntaxToken, + group_layout: GroupedCallArgumentLayout, + f: &mut RFormatter, + ) -> FormatResult<()> { + let grouped_breaks = { + let (grouped_arg, other_args) = match group_layout { + GroupedCallArgumentLayout::GroupedFirstArgument => { + let (first, tail) = arguments.split_at_mut(1); + (&mut first[0], tail) + } + GroupedCallArgumentLayout::GroupedLastArgument => { + let end_index = arguments.len().saturating_sub(1); + let (head, last) = arguments.split_at_mut(end_index); + (&mut last[0], head) + } + }; + + let non_grouped_breaks = other_args.iter_mut().any(|arg| arg.will_break(f)); + + // If any of the not grouped elements break, then fall back to the variant where + // all arguments are printed in expanded mode. + if non_grouped_breaks { + return write!( + f, + [FormatAllArgsBrokenOut { + call: self, + l_token: &l_token.format(), + leading_holes: &leading_holes, + args: &arguments, + r_token: &r_token.format(), + expand: true, + }] + ); + } + + grouped_arg.will_break(f) + }; + + // We now cache the delimiters tokens. This is needed because `[biome_formatter::best_fitting]` will try to + // print each version first + // tokens on the left + let l_token = l_token.format().memoized(); + + // tokens on the right + let r_token = r_token.format().memoized(); + + // First write the most expanded variant because it needs `arguments`. + let most_expanded = { + let mut buffer = VecBuffer::new(f.state_mut()); + buffer.write_element(FormatElement::Tag(Tag::StartEntry))?; + + write!( + buffer, + [FormatAllArgsBrokenOut { + call: self, + l_token: &l_token, + leading_holes: &leading_holes, + args: &arguments, + r_token: &r_token, + expand: true, + }] + )?; + + buffer.write_element(FormatElement::Tag(Tag::EndEntry))?; + buffer.into_vec() + }; + + // Now reformat the first or last argument if they happen to be an inline function. + // Inline functions in this context apply a custom formatting that removes soft line breaks + // from the parameters. + // + // TODO: The JS approach caches the function body of the "normal" (before soft line breaks + // are removed) formatted function to avoid quadratic complexity if the function's body contains + // another call expression with an inline function as first or last argument. We may want to + // consider this if we have issues here. + let last_index = arguments.len() - 1; + let grouped = arguments + .into_iter() + .enumerate() + .map(|(index, argument)| { + let layout = match group_layout { + GroupedCallArgumentLayout::GroupedFirstArgument if index == 0 => { + Some(GroupedCallArgumentLayout::GroupedFirstArgument) + } + GroupedCallArgumentLayout::GroupedLastArgument if index == last_index => { + Some(GroupedCallArgumentLayout::GroupedLastArgument) + } + _ => None, + }; + + FormatGroupedArgument { + argument, + single_argument_list: last_index == 0, + layout, + } + .memoized() + }) + .collect::>(); + + // Write the most flat variant with the first or last argument grouped + // (but not forcibly expanded) + let _most_flat = { + let snapshot = f.state_snapshot(); + let mut buffer = VecBuffer::new(f.state_mut()); + buffer.write_element(FormatElement::Tag(Tag::StartEntry))?; + + let result = write!( + buffer, + [ + l_token, + format_with(|f| { f.join().entries(leading_holes.iter()).finish() }), + maybe_space(!leading_holes.is_empty() && !grouped.is_empty()), + format_with(|f| { + f.join_with(soft_line_break_or_space()) + .entries(grouped.iter()) + .finish() + }), + r_token + ] + ); + + // Turns out, using the grouped layout isn't a good fit because some parameters of the + // grouped inline function break. In that case, fall back to the all args expanded + // formatting. + // This back tracking is required because testing if the grouped argument breaks in general + // would also return `true` if any content of the function BODY breaks. But, as far as this + // is concerned, it's only interested if any content in just the function SIGNATURE breaks. + if matches!(result, Err(FormatError::PoorLayout)) { + drop(buffer); + f.restore_state_snapshot(snapshot); + + let mut most_expanded_iter = most_expanded.into_iter(); + // Skip over the Start/EndEntry items. + most_expanded_iter.next(); + most_expanded_iter.next_back(); + + return f.write_elements(most_expanded_iter); + } + + buffer.write_element(FormatElement::Tag(Tag::EndEntry))?; + buffer.into_vec().into_boxed_slice() + }; + + // Write the second most flat variant that now forces the group of the first/last argument to expand. + let middle_variant = { + let mut buffer = VecBuffer::new(f.state_mut()); + buffer.write_element(FormatElement::Tag(Tag::StartEntry))?; + + write!( + buffer, + [ + l_token, + format_with(|f| { f.join().entries(leading_holes.iter()).finish() }), + maybe_space(!leading_holes.is_empty() && !grouped.is_empty()), + format_with(|f| { + let mut joiner = f.join_with(soft_line_break_or_space()); + + match group_layout { + GroupedCallArgumentLayout::GroupedFirstArgument => { + joiner.entry(&group(&grouped[0]).should_expand(true)); + joiner.entries(&grouped[1..]).finish() + } + GroupedCallArgumentLayout::GroupedLastArgument => { + let last_index = grouped.len() - 1; + joiner.entries(&grouped[..last_index]); + joiner + .entry(&group(&grouped[last_index]).should_expand(true)) + .finish() + } + } + }), + r_token + ] + )?; + + buffer.write_element(FormatElement::Tag(Tag::EndEntry))?; + buffer.into_vec().into_boxed_slice() + }; + + // If the grouped content breaks, then we can skip the most_flat variant, + // since we already know that it won't be fitting on a single line. + let variants = if grouped_breaks { + write!(f, [expand_parent()])?; + vec![middle_variant, most_expanded.into_boxed_slice()] + } else { + unreachable!("`grouped_breaks` is currently always `true`."); + // vec![most_flat, middle_variant, most_expanded.into_boxed_slice()] + }; + + // SAFETY: Safe because variants is guaranteed to contain >=2 entries: + // * most flat (optional) + // * middle + // * most expanded + // ... and best fitting only requires the most flat/and expanded. + unsafe { + f.write_element(FormatElement::BestFitting( + format_element::BestFittingElement::from_vec_unchecked(variants), + )) + } + } } impl Format for RCallLikeArguments { @@ -166,6 +487,7 @@ impl Format for RCallLikeArguments { return write!( f, [FormatAllArgsBrokenOut { + call: self, l_token: &l_token.format(), leading_holes: &leading_holes, args: &arguments, @@ -181,6 +503,7 @@ impl Format for RCallLikeArguments { return write!( f, [FormatAllArgsBrokenOut { + call: self, l_token: &l_token.format(), leading_holes: &leading_holes, args: &arguments, @@ -191,11 +514,19 @@ impl Format for RCallLikeArguments { } if let Some(group_layout) = arguments_grouped_layout(&items, comments) { - write_grouped_arguments(l_token, leading_holes, arguments, r_token, group_layout, f) + self.write_grouped_arguments( + l_token, + leading_holes, + arguments, + r_token, + group_layout, + f, + ) } else { write!( f, [FormatAllArgsBrokenOut { + call: self, l_token: &l_token.format(), leading_holes: &leading_holes, args: &arguments, @@ -493,323 +824,6 @@ impl Format for FormatCallArgument { } } -/// Writes the function arguments -/// -/// The "grouped" argument is either the first or last argument depending on the -/// `group_layout`, but currently it is always the last one. -/// -/// - If any arguments that aren't the grouped argument *force* a break, then we -/// print in fully expanded mode. -/// -/// - If the grouped argument is an inline function with `parameters` that would -/// *force* a break, then we print in fully expanded mode. We only want to -/// allow forced breaks in a braced expression body. -/// -/// If neither of those trigger fully expanded mode, we best-fit between three -/// possible forms: -/// -/// ## Most expanded -/// -/// The `(`, `)`, and all arguments are within a single `group()`, and that -/// group is marked with `should_expand(true)`. The arguments are wrapped in -/// `soft_block_indent()`, and each argument is separated by a -/// `soft_line_break_or_space()`. Due to the forced expansion, these all -/// become hard indents / line breaks, i.e. the "most expanded" form. -/// -/// Example: -/// -/// ```r -/// map( -/// xs, -/// function(x) { -/// x + 1 -/// } -/// ) -/// ``` -/// -/// ## Most flat -/// -/// Arguments are not grouped, each argument is separated by a -/// `soft_line_break_or_space()`, no forced expansion is done. -/// -/// Special formatting is done for a grouped argument that is an inline -/// function. We remove any soft line breaks in the `parameters`, which -/// practically means the only place it is allowed to break is in the function -/// body (but the break is not forced). -/// -/// Example: -/// -/// ```r -/// # NOTE: Not currently possible, as the `{}` always force a break right now, -/// # but this would be an example if `{}` didn't force a break. -/// map(xs, function(x) {}) -/// ``` -/// -/// This variant is removed from the set if we detect that the grouped argument -/// contains a forced break in the body (if a forced break is found in the -/// parameters, we bail entirely and use the most expanded form, as noted -/// at the beginning of this documentation page). -/// -/// Note that because `{}` currently unconditionally force a break, and because -/// we only go down this path when we have a `{}` to begin with, that means that -/// currently the most flat variant is always removed. There is an -/// `unreachable!()` in the code to assert this. We can't simply remove the -/// `most_flat` code path though, because it is also where we detect if a -/// parameter forces a break, triggering one of our early exists. Additionally, -/// in the future we may allow `{}` to not force a break, meaning this variant -/// may come back into play. -/// -/// ```r -/// # NOTE: We explicitly disallow curly-curly as a groupable argument, -/// # so this case is never considered grouped, and is therefore not an -/// # example of "most flat". -/// group_by(df, {{ by }}) -/// ``` -/// -/// ## Middle variant -/// -/// Exactly the same as "most flat", except that the grouped argument is put -/// in its own `group()` marked with `should_expand(true)`. The soft line breaks -/// are removed from any grouped argument parameters, like with most flat. -/// -/// Example: -/// -/// ```r -/// map(xs, function(x) { -/// x + 1 -/// }) -/// ``` -/// -/// ```r -/// # The soft line breaks are removed from the `parameters`, meaning that this... -/// map(xs, function(x, a_long_secondary_argument = "with a default", and_another_one_here) { -/// x + 1 -/// }) -/// -/// # ...is not allowed to be formatted as... -/// map(xs, function( -/// x, -/// a_long_secondary_argument = "with a default", -/// and_another_one_here -/// ) { -/// x + 1 -/// }) -/// -/// # ...and instead the most expanded form is chosen by best-fitting: -/// map( -/// xs, -/// function( -/// x, -/// a_long_secondary_argument = "with a default", -/// and_another_one_here -/// ) { -/// x + 1 -/// } -/// ) -/// ``` -fn write_grouped_arguments( - l_token: RSyntaxToken, - leading_holes: Vec, - mut arguments: Vec, - r_token: RSyntaxToken, - group_layout: GroupedCallArgumentLayout, - f: &mut RFormatter, -) -> FormatResult<()> { - let grouped_breaks = { - let (grouped_arg, other_args) = match group_layout { - GroupedCallArgumentLayout::GroupedFirstArgument => { - let (first, tail) = arguments.split_at_mut(1); - (&mut first[0], tail) - } - GroupedCallArgumentLayout::GroupedLastArgument => { - let end_index = arguments.len().saturating_sub(1); - let (head, last) = arguments.split_at_mut(end_index); - (&mut last[0], head) - } - }; - - let non_grouped_breaks = other_args.iter_mut().any(|arg| arg.will_break(f)); - - // If any of the not grouped elements break, then fall back to the variant where - // all arguments are printed in expanded mode. - if non_grouped_breaks { - return write!( - f, - [FormatAllArgsBrokenOut { - l_token: &l_token.format(), - leading_holes: &leading_holes, - args: &arguments, - r_token: &r_token.format(), - expand: true, - }] - ); - } - - grouped_arg.will_break(f) - }; - - // We now cache the delimiters tokens. This is needed because `[biome_formatter::best_fitting]` will try to - // print each version first - // tokens on the left - let l_token = l_token.format().memoized(); - - // tokens on the right - let r_token = r_token.format().memoized(); - - // First write the most expanded variant because it needs `arguments`. - let most_expanded = { - let mut buffer = VecBuffer::new(f.state_mut()); - buffer.write_element(FormatElement::Tag(Tag::StartEntry))?; - - write!( - buffer, - [FormatAllArgsBrokenOut { - l_token: &l_token, - leading_holes: &leading_holes, - args: &arguments, - r_token: &r_token, - expand: true, - }] - )?; - - buffer.write_element(FormatElement::Tag(Tag::EndEntry))?; - buffer.into_vec() - }; - - // Now reformat the first or last argument if they happen to be an inline function. - // Inline functions in this context apply a custom formatting that removes soft line breaks - // from the parameters. - // - // TODO: The JS approach caches the function body of the "normal" (before soft line breaks - // are removed) formatted function to avoid quadratic complexity if the function's body contains - // another call expression with an inline function as first or last argument. We may want to - // consider this if we have issues here. - let last_index = arguments.len() - 1; - let grouped = arguments - .into_iter() - .enumerate() - .map(|(index, argument)| { - let layout = match group_layout { - GroupedCallArgumentLayout::GroupedFirstArgument if index == 0 => { - Some(GroupedCallArgumentLayout::GroupedFirstArgument) - } - GroupedCallArgumentLayout::GroupedLastArgument if index == last_index => { - Some(GroupedCallArgumentLayout::GroupedLastArgument) - } - _ => None, - }; - - FormatGroupedArgument { - argument, - single_argument_list: last_index == 0, - layout, - } - .memoized() - }) - .collect::>(); - - // Write the most flat variant with the first or last argument grouped - // (but not forcibly expanded) - let _most_flat = { - let snapshot = f.state_snapshot(); - let mut buffer = VecBuffer::new(f.state_mut()); - buffer.write_element(FormatElement::Tag(Tag::StartEntry))?; - - let result = write!( - buffer, - [ - l_token, - format_with(|f| { f.join().entries(leading_holes.iter()).finish() }), - maybe_space(!leading_holes.is_empty() && !grouped.is_empty()), - format_with(|f| { - f.join_with(soft_line_break_or_space()) - .entries(grouped.iter()) - .finish() - }), - r_token - ] - ); - - // Turns out, using the grouped layout isn't a good fit because some parameters of the - // grouped inline function break. In that case, fall back to the all args expanded - // formatting. - // This back tracking is required because testing if the grouped argument breaks in general - // would also return `true` if any content of the function BODY breaks. But, as far as this - // is concerned, it's only interested if any content in just the function SIGNATURE breaks. - if matches!(result, Err(FormatError::PoorLayout)) { - drop(buffer); - f.restore_state_snapshot(snapshot); - - let mut most_expanded_iter = most_expanded.into_iter(); - // Skip over the Start/EndEntry items. - most_expanded_iter.next(); - most_expanded_iter.next_back(); - - return f.write_elements(most_expanded_iter); - } - - buffer.write_element(FormatElement::Tag(Tag::EndEntry))?; - buffer.into_vec().into_boxed_slice() - }; - - // Write the second most flat variant that now forces the group of the first/last argument to expand. - let middle_variant = { - let mut buffer = VecBuffer::new(f.state_mut()); - buffer.write_element(FormatElement::Tag(Tag::StartEntry))?; - - write!( - buffer, - [ - l_token, - format_with(|f| { f.join().entries(leading_holes.iter()).finish() }), - maybe_space(!leading_holes.is_empty() && !grouped.is_empty()), - format_with(|f| { - let mut joiner = f.join_with(soft_line_break_or_space()); - - match group_layout { - GroupedCallArgumentLayout::GroupedFirstArgument => { - joiner.entry(&group(&grouped[0]).should_expand(true)); - joiner.entries(&grouped[1..]).finish() - } - GroupedCallArgumentLayout::GroupedLastArgument => { - let last_index = grouped.len() - 1; - joiner.entries(&grouped[..last_index]); - joiner - .entry(&group(&grouped[last_index]).should_expand(true)) - .finish() - } - } - }), - r_token - ] - )?; - - buffer.write_element(FormatElement::Tag(Tag::EndEntry))?; - buffer.into_vec().into_boxed_slice() - }; - - // If the grouped content breaks, then we can skip the most_flat variant, - // since we already know that it won't be fitting on a single line. - let variants = if grouped_breaks { - write!(f, [expand_parent()])?; - vec![middle_variant, most_expanded.into_boxed_slice()] - } else { - unreachable!("`grouped_breaks` is currently always `true`."); - // vec![most_flat, middle_variant, most_expanded.into_boxed_slice()] - }; - - // SAFETY: Safe because variants is guaranteed to contain >=2 entries: - // * most flat (optional) - // * middle - // * most expanded - // ... and best fitting only requires the most flat/and expanded. - unsafe { - f.write_element(FormatElement::BestFitting( - format_element::BestFittingElement::from_vec_unchecked(variants), - )) - } -} - /// Helper for formatting the first grouped argument (see [should_group_first_argument]). struct FormatGroupedFirstArgument<'a> { argument: &'a FormatCallArgument, @@ -918,6 +932,7 @@ impl Format for FormatGroupedArgument { } struct FormatAllArgsBrokenOut<'a> { + call: &'a RCallLikeArguments, l_token: &'a dyn Format, leading_holes: &'a [FormatCallArgumentHole], args: &'a [FormatCallArgument], @@ -944,13 +959,19 @@ impl Format for FormatAllArgsBrokenOut<'_> { Ok(()) }); + let args = if !self.expand && is_hugging_call(self.args, self.call.r_token())? { + Either::Left(args) + } else { + Either::Right(soft_block_indent(&args)) + }; + write!( f, [group(&format_args![ self.l_token, format_with(|f| f.join().entries(self.leading_holes.iter()).finish()), maybe_space(!self.leading_holes.is_empty() && !self.args.is_empty()), - soft_block_indent(&args), + &args, self.r_token, ]) .should_expand(self.expand)] @@ -958,6 +979,39 @@ impl Format for FormatAllArgsBrokenOut<'_> { } } +fn is_hugging_call( + items: &[FormatCallArgument], + r_token: SyntaxResult, +) -> SyntaxResult { + // We only consider calls with one argument + let Some(item) = items.first() else { + return Ok(false); + }; + if items.len() != 1 { + return Ok(false); + } + + // Unwrap the value to get the `AnyRExpression` + let Some(arg) = item.element().node.as_ref().ok() else { + return Ok(false); + }; + let Some(arg) = arg.value() else { + return Ok(false); + }; + + // Bail on hugging if the argument has comments attached. In practice only + // trailing comments get reordered so we don't need to check for comments on + // the argument name. + if arg.syntax().has_comments_direct() || r_token.is_ok_and(|tok| tok.has_leading_comments()) { + return Ok(false); + } + + Ok(matches!( + arg, + AnyRExpression::RCall(_) | AnyRExpression::RSubset(_) | AnyRExpression::RSubset2(_) + )) +} + #[derive(Copy, Clone, Debug)] pub enum GroupedCallArgumentLayout { /// Group the first call argument. diff --git a/crates/air_r_formatter/tests/specs/r/call.R b/crates/air_r_formatter/tests/specs/r/call.R index 28b7b57a..da4d0401 100644 --- a/crates/air_r_formatter/tests/specs/r/call.R +++ b/crates/air_r_formatter/tests/specs/r/call.R @@ -641,3 +641,142 @@ fn( a, b ) + +# ------------------------------------------------------------------------ +# Hugging calls - https://github.com/posit-dev/air/issues/21 + +# Motivating hugging cases +abort(glue::glue("Length implied by `dim`, {n_elements}, must match the length of `x`, {n_x}.")) +abort(paste0("This is a section", and, "this is another section", "and this is a final section")) + +# Single line +c(list(1)) + +# Persistent newline +c( + list(1) +) + +# Symbol: Line length expansion +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz)) + +# Call: Recursive hugging case, no breaks +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz())) + +# Call: Recursive hugging case, inner arguments break +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz(1, 2))) + +# Call: Recursive hugging case, persistent newlines +c(list(foobar( + 1, + 2 +))) + +# Sanity checks for comments + +c( + #foo + list( + 1 + ) +) + +c( + list( + 1 + ) + #foo +) + +c(list( + #foo + 1 +)) + +c(list( + #foo + x = 1 +)) + +c(list( + x = + #foo + 1 +)) + +c(list( + #foo +)) + +# Trailing comment of inner paren +c(list( + 1 +) #foo +) + +# Leading comment of outer paren +c(list( + 1 +) +#foo +) + +c( + list( + 1 + ) #foo +) + +# Leading holes +fn(,, paste0("This is a section", and, "this is another section", "and this is a final section")) + +fn[,, paste0("This is a section", and, "this is another section", "and this is a final section")] + +# Subsetting +foo(bar[ + 1, + 2 +]) + +foo[[bar( + 1, + 2 +)]] + +# Fits on one line +foo[[bar(1, 2)]] + +# Persistent line +foo[[ +bar( + 1, + 2 +)]] + +foo( + #foo + bar[ + 1, + 2 +]) + +foo(bar[ + 1, + 2 +] +#foo +) + +foo(bar[ + 1, + 2 + #foo +] +) + +foo( bar[ + #foo + 1, + 2 +] +) diff --git a/crates/air_r_formatter/tests/specs/r/call.R.snap b/crates/air_r_formatter/tests/specs/r/call.R.snap index 9aa7b175..286250cd 100644 --- a/crates/air_r_formatter/tests/specs/r/call.R.snap +++ b/crates/air_r_formatter/tests/specs/r/call.R.snap @@ -649,6 +649,145 @@ fn( b ) +# ------------------------------------------------------------------------ +# Hugging calls - https://github.com/posit-dev/air/issues/21 + +# Motivating hugging cases +abort(glue::glue("Length implied by `dim`, {n_elements}, must match the length of `x`, {n_x}.")) +abort(paste0("This is a section", and, "this is another section", "and this is a final section")) + +# Single line +c(list(1)) + +# Persistent newline +c( + list(1) +) + +# Symbol: Line length expansion +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz)) + +# Call: Recursive hugging case, no breaks +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz())) + +# Call: Recursive hugging case, inner arguments break +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz(1, 2))) + +# Call: Recursive hugging case, persistent newlines +c(list(foobar( + 1, + 2 +))) + +# Sanity checks for comments + +c( + #foo + list( + 1 + ) +) + +c( + list( + 1 + ) + #foo +) + +c(list( + #foo + 1 +)) + +c(list( + #foo + x = 1 +)) + +c(list( + x = + #foo + 1 +)) + +c(list( + #foo +)) + +# Trailing comment of inner paren +c(list( + 1 +) #foo +) + +# Leading comment of outer paren +c(list( + 1 +) +#foo +) + +c( + list( + 1 + ) #foo +) + +# Leading holes +fn(,, paste0("This is a section", and, "this is another section", "and this is a final section")) + +fn[,, paste0("This is a section", and, "this is another section", "and this is a final section")] + +# Subsetting +foo(bar[ + 1, + 2 +]) + +foo[[bar( + 1, + 2 +)]] + +# Fits on one line +foo[[bar(1, 2)]] + +# Persistent line +foo[[ +bar( + 1, + 2 +)]] + +foo( + #foo + bar[ + 1, + 2 +]) + +foo(bar[ + 1, + 2 +] +#foo +) + +foo(bar[ + 1, + 2 + #foo +] +) + +foo( bar[ + #foo + 1, + 2 +] +) + ``` @@ -1342,9 +1481,175 @@ fn( a, b ) + +# ------------------------------------------------------------------------ +# Hugging calls - https://github.com/posit-dev/air/issues/21 + +# Motivating hugging cases +abort(glue::glue( + "Length implied by `dim`, {n_elements}, must match the length of `x`, {n_x}." +)) +abort(paste0( + "This is a section", + and, + "this is another section", + "and this is a final section" +)) + +# Single line +c(list(1)) + +# Persistent newline +c( + list(1) +) + +# Symbol: Line length expansion +c(list( + foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz +)) + +# Call: Recursive hugging case, no breaks +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz())) + +# Call: Recursive hugging case, inner arguments break +c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz( + 1, + 2 +))) + +# Call: Recursive hugging case, persistent newlines +c(list(foobar( + 1, + 2 +))) + +# Sanity checks for comments + +c( + #foo + list( + 1 + ) +) + +c( + list( + 1 + ) + #foo +) + +c(list( + #foo + 1 +)) + +c(list( + #foo + x = 1 +)) + +c(list( + #foo + x = 1 +)) + +c(list( + #foo +)) + +# Trailing comment of inner paren +c( + list( + 1 + ) #foo +) + +# Leading comment of outer paren +c( + list( + 1 + ) + #foo +) + +c( + list( + 1 + ) #foo +) + +# Leading holes +fn(,, paste0( + "This is a section", + and, + "this is another section", + "and this is a final section" +)) + +fn[,, paste0( + "This is a section", + and, + "this is another section", + "and this is a final section" +)] + +# Subsetting +foo(bar[ + 1, + 2 +]) + +foo[[bar( + 1, + 2 +)]] + +# Fits on one line +foo[[bar(1, 2)]] + +# Persistent line +foo[[ + bar( + 1, + 2 + ) +]] + +foo( + #foo + bar[ + 1, + 2 + ] +) + +foo( + bar[ + 1, + 2 + ] + #foo +) + +foo(bar[ + 1, + 2 + #foo +]) + +foo(bar[ + #foo + 1, + 2 +]) ``` # Lines exceeding max width of 80 characters ``` 307: my_long_list_my_long_list_my_long_list_my_long_list_long_long_long_long_long_list, + 701: foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz + 705: c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz())) + 708: c(list(foobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbafoobarbazzzzzzzzzfoobarbaz( ```