---
title: "Formatter"
editor:
  markdown:
    wrap: sentence
    canonical: true
---

Air is first and foremost a formatter of R code.
On this page, you'll find details about what a formatter is, why you'd want to use one, and you'll learn about features that help you anticipate how Air will format your R code.

# What's a formatter?

A formatter is in charge of the *layout* of your R code.
Formatters do not change the meaning of code; instead they ensure that whitespace, newlines, and other punctuation conform to a set of rules and standards, such as:

-   Making sure your code is **indented** with the appropriate amount of leading whitespace depending on the context.
    By default, Air uses an indentation of 2 spaces.
    You will see this indentation in pipelines:

    ``` r
    data |>
      ggplot(aes(x, y)) +
      geom_point()
    ```

    as well as *expanded* (i.e. vertically laid out) function calls:

    ``` r
    list(
      foo = 1,
      bar = 2
    )
    ```

-   Preventing your code from overflowing a given **line width**.
    By default, we use a line width of 80 characters.
    It does so by splitting lines of code that have become too long over multiple lines.
    For instance, let's say that we've set the line width to be extremely small and these expressions would overflow:

    ``` r
    data |> select(foo)

    foo <- function(bar = 1, baz = 2) {
      list(bar, baz)
    }
    ```

    To respect the very small line width, Air would switch these expressions from a horizontal layout (called "flat") to a vertical one (called "expanded"):

    ``` r
    data |>
      select(foo)

    foo <- function(
      bar = 1,
      baz = 2
    ) {
      list(
        bar,
        baz
      )
    }
    ```

-   Standardizing the whitespace around code elements.
    Have you ever had difficulties deciphering very dense code?

    ``` r
    1+2:3*(4/5)
    ```

    Air reformats this expression to:

    ``` r
    1 + 2:3 * (4 / 5)
    ```

In general, a formatter takes over the whitespace in your code and moves elements around to respect style conventions and maximize readability.

# How does a formatter help your workflow?

By using a formatter it might seem like you're rescinding control over the layout of your code.
And indeed you are!
However, putting Air in charge of styling your code has substantial advantages.

First, it automatically forces you to write legible code that is neither too wide nor too narrow, with proper breathing room around syntactic elements.
Having a formatter as a companion significantly improves the process of writing code as you no longer have to think about style as much - the formatter does that for you!

Second, it reduces friction when working in a team.
By agreeing to use a formatter in a project, collaborators no longer have to discuss styling and layout issues.
Code sent to you by a colleague will adhere to the standards that you're used to.
Code review no longer has to be about style nitpicks and can focus on the substance of the changes instead.

# When does a formatter run?

The most convenient way to run a formatter is when you save a file.
Most editors don't enable this by default, so check out the [Editors](editors.qmd) section to learn how to configure your editor to format on save.
Once enabled, your code will be reformatted every time you save!
This prevents your code from ever diverging from the standards set by Air.

Air also provides a [CLI tool](cli.qmd).
It makes it easy to reformat whole directories, such as an R project or package, using a single call to `air format <path>`.

Note that in both of these cases, the formatter will only run if your code is syntactically correct.
If the formatter is seemingly no longer working, check your code for syntax errors.

# Persistent line breaks

Air tries to strike a balance between enforcing rigid rules and allowing authors some control over the layout.
Our main source of styling rules is the [Tidyverse style guide](https://style.tidyverse.org), but we occasionally deviate from these.

There is a trend among modern formatters of being *opinionated*.
Air certainly fits this trend and provides very few [configuration options](configuration.qmd), mostly the indent style (spaces versus tabs), the indent width, and the line width.
However, Air also puts code authors in charge of certain aspects of the layout through the notion of **persistent line breaks**.

In general, Air is in control of deciding where to put vertical space (line breaks) in your code.
For instance if you write:

``` r
list(foo,
bar)
```

Air will figure out that this expression fits on a single line without exceeding the line width.
It will discard the line break and reformat to:

``` r
list(foo, bar)
```

However there are very specific places at which you can enforce a line break, i.e. make it persistent.

-   Before the very first argument in a function call.
    This:

    ``` r
    list(
    foo, bar)
    ```

    gets formatted as:

    ``` r
    list(
      foo,
      bar
    )
    ```

-   Before the very first right-hand side expression in a pipeline.
    This:

    ``` r
    data |>
    select(foo) |> filter(!bar)
    ```

    gets formatted as:

    ``` r
    data |>
      select(foo) |>
      filter(!bar)
    ```

A persistent line break will never be removed by Air.
But you can remove it manually.
Taking the last example, if you join the first lines like this:

``` r
list(foo,
  bar
)

1 + 2 +
  3
```

Air will recognize that you've removed the persistent line break, and reformat as:

``` r
list(foo, bar)

1 + 2 + 3
```

The goal of this feature is to strike a balance between being opinionated and recognizing that users often know when taking up more vertical space results in more readable output.

# Autobracing

To encourage more consistent, readable, and portable code, Air will *autobrace* the following elements:

-   If statements

-   For, while, and repeat loops

-   Function definitions

Autobracing is the process of wrapping the body of these code elements with `{ }` if braces don't already exist.

## If statements

Air will autobrace if statements if:

-   Any existing part of the if statement spans multiple lines

-   Any existing part of the if statement is already braced

-   The if statement is nested, i.e. there is an `else if {`

-   The if statement exceeds the line length

For example, the following will all be autobraced:

``` r
if (condition)
  a

# Becomes:
if (condition) {
  a
}
```

``` r
if (condition) a else { b }

# Becomes:
if (condition) {
  a
} else {
  b
}
```

``` r
if (condition) a else if (condition2) b else c

# Becomes:
if (condition) {
  a
} else if (condition2) {
  b
} else {
  c
}
```

Simple if statements that don't hit any of the autobracing criteria mentioned above are allowed to stay on one line as long as they are also in *value* position, as opposed to *effect* position.

-   Top level if statements are in effect position.

-   If statements that are direct children of `{}` are in effect position, unless the if statement is the last child of the `{}` expression list, in which case it is in value position (because it is the returned value from that scope).

-   Otherwise, the if statement is in value position.

This if statement is at top level, putting it in effect position, and would be autobraced:

``` r
if (condition) a else b

# Becomes:
if (condition) {
  a
} else {
  b
}
```

These if statements are children of `{}` (but aren't the last child!), putting them in effect position, and would be autobraced:

``` r
fn <- function(x) {
  if (condition) stop("oh no")
  if (condition) return(1)
  if (condition) x <- 1
  x + 1
}

# Becomes:
fn <- function(x) {
  if (condition) {
    stop("oh no")
  }
  if (condition) {
    return(1)
  }
  if (condition) {
    x <- 1
  }
  x + 1
}
```

It's particularly important for code involving `stop()`, `return()`, and `<-` to be easily readable on their own line because they cause *side effects* that affect control flow or state.

These if statements are in value position, and would not be autobraced:

``` r
x <- if (condition) 1 else 2

x <- x %||% if (condition) 1 else 2

list(a = if (condition) 1 else 2)

function(
  a,
  optional = if (is.null(a)) 1 else 2
) {
}

# If statement is the last expression of the `{}` scope
map(xs, function(x) {
  if (is.null(x)) 1 else 2
})
```

### Portability

It is particularly important to autobrace multiline if statements for *portability*, which is the ability to copy and paste that if statement into any context and have it still parse.
Consider the following if statement:

``` r
fn <- function(a) {
  if (is.null(a))
    1
  else
    2
}
```

This parses and runs correctly while the if statement is nested within the `{}` braces of the function.
But if you're testing this code and you copy and paste it out of the function, then it no longer parses:

``` r
if (is.null(a))
  1
else
  2
```

If you try and run this, then you'll see an error like `Error: unexpected 'else'`.
This is particularly annoying when you're working inside a debugger.
Most R debuggers allow you to pause inside functions and highlight and run chunks of that function.
If you're paused inside `fn()` and try to highlight and run the if statement, then it will confusingly fail to parse.
Autobracing multiline if statements avoids this problem entirely.

## For, while, and repeat loops

Air unconditionally autobraces the body of all R loops.
This is mostly for consistency with if statements, as it is fairly uncommon to see braceless loops in practice.

``` r
for (i in 1:5) x <- x + i

# Becomes:
for (i in 1:5) {
  x <- x + i
}
```

``` r
while (x < 5) x <- x + 1

# Becomes:
while (x < 5) {
  x <- x + 1
}
```

## Function definitions

Air will autobrace the body of a function definition if:

-   Any existing part of the function definition spans multiple lines

-   The function definition exceeds the line length

``` r
fn <- function(a, b)
  a + b

# Becomes:
fn <- function(a, b) {
  a + b
}
```

``` r
fn <- function(
  a,
  b
) a + b

# Becomes:
fn <- function(
  a,
  b
) {
  a + b
}
```

``` r
fn <- function(a_really_long_variable_name, another_really_long_name) a_really_long_variable_name + another_really_long_name

# Becomes:
fn <- function(
  a_really_long_variable_name,
  another_really_long_name
) {
  a_really_long_variable_name + another_really_long_name
}
```

Short function definitions are allowed on one line and will not be autobraced.
These are all allowed by Air:

``` r
add_one <- function(x) x + 1

map_lgl(xs, function(x) is.logical(x) && length(x) == 1L && !is.na(x))

# This includes anonymous functions
map_lgl(xs, \(x) is.list(x) && length(x) == 0L)
```

## With persistent line breaks

Autobracing is particularly useful as a code rewriting tool when combined with persistent line breaks.
Consider:

``` r
result <- map_lgl(xs, function(x) is.logical(x) && length(x) == 1L && !is.na(x))
```

This may be easier to read if it spanned across multiple lines.
You could manually rework this, or you could let Air help you!
There are two places you could put a persistent line break depending on what your desired final result is:

``` r
# Adding a line break before `xs` expands the call
result <- map_lgl(
  xs, function(x) is.logical(x) && length(x) == 1L && !is.na(x))

# Becomes:
result <- map_lgl(
  xs,
  function(x) is.logical(x) && length(x) == 1L && !is.na(x)
)
```

``` r
# Adding a line break before `is.logical(x)` forces autobracing
result <- map_lgl(xs, function(x)
    is.logical(x) && length(x) == 1L && !is.na(x))

# Becomes:
result <- map_lgl(xs, function(x) {
  is.logical(x) && length(x) == 1L && !is.na(x)
})
```

## Comments

Air generally avoids moving your comments.
However, when Air autobraces code, it may have to adjust them.
This generally works quite well for most code, but is impossible to do perfectly.
It is possible that you will have to adjust the placement of your comments after Air runs.

For example, leading comments on autobraced elements are generally placed in a way that you'd expect:

``` r
if (condition)
  # My comment
  a

# Becomes:
if (condition) {
  # My comment
  a
}
```

But trailing comments might need manual adjustment:

``` r
if (condition)
  a # My comment

# Becomes:
if (condition) {
  a
} # My comment

# You may want to adjust it to:
if (condition) {
  a # My comment
}
```

In general, prefer leading comments over trailing comments for readability and to have the highest chance of Air placing it in the correct location when comment adjustment is required.

# Empty braces

Braced expressions generally span multiple lines:

``` r
for (x in xs) {
  print(x)
}

function(x, y) {
  x + y
}

function() {
  # Do nothing
}
```

The one exception to this is completely empty braces, i.e. `{}`.
These are never expanded, allowing for the following style:

``` r
dummy <- function() {}

while (waiting()) {}

switch(x, a = {}, b = 2)

function(
  expr = {}
) {
  this_first()
  expr
}
```

# Disabling formatting

## Skip comments

Air supports two special comments to disable formatting, `# fmt: skip` and `# fmt: skip file`.

`# fmt: skip` skips formatting for the following syntax node (including all of its children).

``` r
# This skips formatting for `list()` and its arguments, retaining the manual alignment
# fmt: skip
list(
  dollar = "USA",
  yen    = "Japan",
  yuan   = "China"
)

# This skips formatting for `tribble()` and its arguments
# fmt: skip
tribble(
  ~x, ~y,
   1,  2,
)

# This skips formatting for `graph_from_literal()` and its arguments,
# retaining the formatting of the domain specific language
# fmt: skip
igraph::graph_from_literal(Alice +--+ Bob)
```

`# fmt: skip file` skips the entire file.
This must be placed at the top of the file before the first non-comment R code, otherwise the comment is ignored.
This is useful for generated files, or as an alternative to `exclude` for individual files that you know you'd like to exclude from formatting.

``` r
# This is a generated file. Don't modify by hand!
# fmt: skip file

# This won't be formatted
generated_function_signature <- function(which, might, overflow, the, line, width, but, we, dont, really, care) {
    body
}

# Neither will this
another_generated_function_signature <- function(which, might, overflow, the, line, width, but, we, dont, really, care) {
    body
}
```

For generated files we also support the convention of a comment starting with `Generated by` at the top of the file:

```
# Generated by myPackage: don't modify by hand
```

## Skip configuration

Skip comments are useful for disabling formatting for one-off function calls, but sometimes you may find yourself repeatedly using functions from a domain specific language (DSL) that doesn't follow conventional formatting rules.
For example, the igraph package contains a DSL for constructing a graph from a literal representation:

``` r
igraph::graph_from_literal(A +-+ B +---+ C ++ D + E)
```

Air would format this as:

``` r
igraph::graph_from_literal(A + -+B + ---+C + +D + E)
```

If you use `graph_from_literal()` often, it would be annoying to add `# fmt: skip` comments at every call site.
Instead, `air.toml` supports a [`skip` field](configuration.qmd#configuration-skip) that allows you to specify function names that you never want formatting for.
Specifying this would retain the original formatting of the `graph_from_literal()` call, even without a `# fmt: skip` comment:

``` toml
skip = ["graph_from_literal"]
```

## Line endings

When using `# fmt: skip`, `# fmt: skip file`, or `skip` in `air.toml`, line endings are never considered skipped.

For example, if you've set `line-ending = "lf"` in `air.toml` and have the following file with (invisible) CRLF line endings:

``` r
1+1

# fmt: skip
fn(
  a, b
)
```

Then the visible text making up the multiline call to `fn()` will be left as is, but the invisible CRLF line endings following `fn(` and `b` will be converted to LF line endings, even though they belong to the skipped node. Without this, you'd end up with a file containing mixed line endings, which would be very confusing!

The only place this may be surprisingly is with `# fmt: skip file`, where the visible text within the file is untouched, but the line endings may be rewritten to match the `line-ending` option.
Since `# fmt: skip file` is an extension of `# fmt: skip` to all nodes, we consider this behavior to be consistent.
If you'd like Air to completely ignore a file, use an `air.toml` level `exclude` instead.

# Comment directives

With `# fmt: skip` we've already seen one kind of comment directive, but that's not all.
Air supports another directive, `# fmt: table`, and we plan to add more in the future.

In general, comment directives are a way to opt out of Air's default automatic formatting and opt into specialized format layouts.
They generally apply to the expression below the comment, with some possible nuances.

-   `skip`: Applies to any following expression.
-   `table`: Applies to the next expression if a function call (e.g. `foo()` or an assigned function call (e.g. `foo <- bar()`)).

## Table formatting

::: callout-caution
## Note: This feature is experimental. We'd be grateful for any feedback!
:::

This directive is particularly useful with calls like tibble's `tribble()` or data.table's `fcase()`.
It formats calls taking unnamed arguments via `...` in a tabular way.

-   Commas are aligned, creating column guides
-   Numeric values are right-aligned, making them easier to read and compare
-   Decimals are aligned, making it easier to distinguish integer and fractional parts
-   Everything else is left-aligned, making it easier to read from left to right
-   Complex calls are formatted in a flat layout, avoiding line breaks where possible.

``` r
tribble(
  ~customer   , ~product   , ~price  , ~units_sold ,
  "ACME"      , "Computer" , 1499.99 ,          25 ,
  "Globex"    , "Monitor"  ,  889.5  ,        1005 ,
  "Stark"     , "Tablet"   ,   99.99 ,         304 ,
  "WayneEnt." , "Tablet"   ,  259    ,          40 ,
)
```

In addition:

-   The layout of the table is never changed by the formatter. It never introduces line breaks that don't already exist to avoid messing up the tabular data, which could be hard to recover from.
-   Tables are allowed to be jagged with different numbers of columns per rows. This allows formatting partial tables as you write them.

Note that if a complex expression contains a line break that Air can't flatten out, tabular formatting is disabled.
You'll have to find the offending argument and make it flat to turn table formatting back on.

By default, all calls to `tribble()` and `fcase()` are formatted as tables, even without a `# fmt: table` directive.
Two [configuration options](configuration.qmd#configuration-table) help you tweak this default behavior:

-   `table`: Extend the default table calls (e.g. `tribble()`) with custom function names that should be formatted as tables across your whole project without requiring a `# fmt: table` directive.

-   `default-table`: Whether to turn off Air's default for `tribble()` and `fcase()`.
