Skip to content

Commit

Permalink
Add support for magic line breaks in left assignment (#118)
Browse files Browse the repository at this point in the history
* Add support for magic line breaks in left assignment

* Add changelog bullet
  • Loading branch information
DavisVaughan authored Dec 20, 2024
1 parent d61f962 commit c4f0861
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 48 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# Development version

- Magic line breaks are now supported in left assignment (#118).


# 0.1.1

Expand Down
93 changes: 91 additions & 2 deletions crates/air_r_formatter/src/r/auxiliary/binary_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,113 @@ fn fmt_binary_sticky(
}

/// Assignment expressions keep LHS and RHS on the same line, separated by a single space
///
/// # Magic line breaks
///
/// The one exception to this is if a newline already exists between a left assignment
/// operator and the rhs, which is a magic line break case, like:
///
/// ```r
/// response <-
/// if (condition) {
/// "yes"
/// } else {
/// "no"
/// }
///
/// resolved <-
/// this %||%
/// complex_that(a, b, c, d) %||%
/// complex_that(e, f, g, h) %||%
/// default()
/// ```
///
/// Walrus assignment is not considered when looking for magic line breaks because we
/// don't want the `:=` case below to look like a request for expansion. While `:=` is
/// technically parsed as a binary operator, we format it more like a named argument with
/// a simple `space()` between the operator and the right hand side.
///
/// ```r
/// # `:=` here is technically a binary operator, `x := y` is technically an unnamed argument
/// fn(
/// x :=
/// y
/// )
///
/// # `=` here is not a binary operator, `x = y` is a named argument and `y` will be
/// # forced onto the same line as `=`. We want to treat `:=` like this.
/// fn(
/// x =
/// y
/// )
/// ```
///
/// Comment handling with magic line breaks here is a bit tricky. Consider this example:
///
/// ```r
/// x <-
/// y # comment
/// ```
///
/// Note that `# comment` is actually attached to the whole binary expression node. When
/// determining default comment placement, the [DecoratedComment::enclosing_node()] is the
/// root node here, making the [DecoratedComment::preceding_node()] the binary expression
/// node.
///
/// This means we can't use a simple [block_indent()] on `right`. The `block_indent()`
/// would force a newline after the `y` but before the comment, moving the comment to the
/// next line.
///
/// Instead, we do the same trick that we do in [fmt_binary_chain()] (and in Biome with
/// binary chains) of using the "rarely needed" manual [indent()] and adding a leading
/// [hard_line_break()] but not a trailing one. By avoiding a trailing hard line break,
/// the trailing comment is allowed to be formatted on the same line as `y`.
fn fmt_binary_assignment(
left: AnyRExpression,
operator: SyntaxToken<RLanguage>,
right: AnyRExpression,
f: &mut Formatter<RFormatContext>,
) -> FormatResult<()> {
let right = format_with(|f| {
if binary_assignment_needs_user_requested_expansion(&operator, &right) {
write!(
f,
[indent(&format_args![hard_line_break(), right.format()])]
)
} else {
write!(f, [space(), right.format()])
}
});

write!(
f,
[group(&format_args![
left.format(),
space(),
operator.format(),
space(),
right.format()
right
])]
)
}

fn binary_assignment_needs_user_requested_expansion(
operator: &SyntaxToken<RLanguage>,
right: &AnyRExpression,
) -> bool {
// TODO: This should be configurable by an option, since it is a case of
// irreversible formatting

// Only for these kinds of left assignment
if !matches!(
operator.kind(),
RSyntaxKind::EQUAL | RSyntaxKind::ASSIGN | RSyntaxKind::SUPER_ASSIGN
) {
return false;
}

right.syntax().has_leading_newline()
}

/// Format a binary expression
///
/// These expressions are not chainable, they use a simple
Expand Down
86 changes: 71 additions & 15 deletions crates/air_r_formatter/tests/specs/r/binary_expression.R
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,6 @@ argument_that_is_quite_long + argument_that_is_quite_long^argument_that_is_quite
1 ^ 2
1 : 2

# The following assignments should start the LHS/RHS on the same
# line as the operator
fn = function(x) {
x
}
fn <- function(x) {
x
}
fn <<- function(x) {
x
}

identity(1) -> x
identity(1) ->> x

# -----------------------------------------------------------------------------
# Help specific

Expand Down Expand Up @@ -452,3 +437,74 @@ ggplot() +
geom_line() +
geom_bar() %>%
identity()

# -----------------------------------------------------------------------------
# Assignment

# The following assignments should start the LHS/RHS on the same
# line as the operator
fn = function(x) {
x
}
fn <- function(x) {
x
}
fn <<- function(x) {
x
}

# Assignment comment tests
fn <- function(x) # comment1
{ # comment2
x # comment3
} # comment4

identity(1) -> x
identity(1) ->> x

# -----------------------------------------------------------------------------
# Assignment with magic line breaks

# Magic line break after the left assignment
fn =
value
fn <-
value
fn <<-
value

# Important that comment3 trails `value` here!
fn <- # comment1
# comment2
value # comment3

# No magic line break after walrus operator
fn :=
value

# We want these to match, neither support magic line breaks
call(fn :=
value)
call(fn =
value)

# No magic line break after right assignment
fn ->
value
fn ->>
value

# https://github.com/posit-dev/air/issues/91
is_condition_true <-
if (condition) {
"yes"
} else {
"no"
}

# https://github.com/posit-dev/air/issues/91
base_version <-
version %||%
b_get(brand, "defaults", "shiny", "theme", "version") %||%
b_get(brand, "defaults", "bootstrap", "version") %||%
version_default()
Loading

0 comments on commit c4f0861

Please sign in to comment.