We’re very excited to announce Air 0.7.0, a new release of our extremely fast R formatter. This post will act as a roundup of releases 0.5.0 through 0.7.0, including: even better Positron support, a new feature called autobracing, and an official GitHub Action! If you haven’t heard of Air, read our announcement blog post first to get up to speed. To install Air, read our editors guide.
Positron
The Air extension is now included in Positron by default, and will automatically keep itself up to date. We’ve been working hard to ensure that Air leaves a positive first impression, and we think that having Positron come batteries included with Air really helps with that! Positron now also ships with Ruff, the extremely fast Python formatter and linter, ensuring that you have a great editing experience out of the box, no matter which language you prefer.
We’ve also streamlined the process of adding Air to a new or existing project. With dev usethis, you can now run
usethis::use_air()
to automatically configure recommended Air settings. In particular, this will:
-
Create an empty
air.toml
. -
Create
.vscode/settings.json
filled with the following settings. This enablesFormat on Save
within your workspace.{ "[r]": { "editor.formatOnSave": true, "editor.defaultFormatter": "Posit.air-vscode" } }
-
Create
.vscode/extensions.json
filled with the following settings. This automatically prompts contributors that don’t have the Air extension to install it when they open your workspace, ensuring that everyone is using the same formatter!{ "recommendations": [ "Posit.air-vscode" ] }
-
Update your
.Rbuildignore
to exclude Air related configuration, if you’re working on an R package.
Once you’ve used usethis to configure Air, you can now immediately reformat your entire workspace by running Air: Format Workspace Folder
from the Command Palette (accessible via Cmd + Shift + P
on Mac/Linux, or Ctrl + Shift + P
on Windows). I’ve found that this is invaluable for adopting Air in an existing project!
To summarize, we’ve reduced our advice on adding Air to an existing project down to:
-
Open Positron
-
Run
Air: Format Workspace Folder
-
Commit, push, and then enjoy using
Format on Save
forevermore 😄
More editors!
Positron isn’t the only editor that’s received some love! We now have official documentation for using Air in the following editors:
We’re very proud of the fact that Air can be used within any editor, not just RStudio and Positron! This documentation was a community effort - thanks in particular to @taplasz, @PMassicotte, @m-muecke, @TymekDev, and @wurli.
Autobracing
Autobracing is the process of adding braces (i.e.Â
{ }
) to if statements, loops, and function definitions to create more consistent, readable, and portable code. It looks like this:
for (i in seq_along(x)) x[[i]] <- x[[i]] + 1L
# Becomes:
for (i in seq_along(x)) {
x[[i]] <- x[[i]] + 1L
}
function(x, y)
call_that_spans_lines(
x,
y,
fixed_option = FALSE
)
# Becomes:
function(x, y) {
call_that_spans_lines(
x,
y,
fixed_option = FALSE
)
}
It’s particularly important to autobrace multiline if statements for portability, which we roughly define as the ability to copy and paste that if statement into any context and have it still parse correctly. Consider the following if statement:
do_something <- function(this = TRUE) {
if (this)
do_this()
else
do_that()
}
As written, this is correct R code, but if you were to pull out the if statement and place it in a file at “top level” and try to run it, you’d see a parse error:
if (this)
do_this()
else
do_that()
#> Error: unexpected 'else'
In practice, this typically bites you when you’re debugging and you send a chunk of lines to the console:
Air autobraces this if statement to the following, which has no issues with portability:
do_something <- function(this = TRUE) {
if (this) {
do_this()
} else {
do_that()
}
}
Give side effects some Air
We believe code that create side effects which modify state or affect control flow are important enough to live on their own line. For example, the following
stop()
call is an example of a side effect, so it moves to its own line and is autobraced:
if (anyNA(x)) stop("`x` can't contain missing values.")
# Becomes:
if (anyNA(x)) {
stop("`x` can't contain missing values.")
}
You might be thinking, “But I like my single line if statements!” We do too! Air still allows single line if statements if they look to be used for their value rather than for their side effect. These single line if statements are still allowed:
x <- if (condition) this else that
x <- x %||% if (condition) this else that
list(a = if (condition) this else that)
Similarly, single line function definitions are also still allowed if they don’t already have braces and don’t exceed the line length:
add_one <- function(x) x + 1
bools <- map_lgl(xs, function(x) is.logical(x) && length(x) == 1L && !is.na(x))
For the full set of rules, check out our documentation on autobracing.
Empty braces
You may have noticed the following forced expansion of empty
{}
in previous versions of Air:
dummy <- function() {}
# Previously became:
dummy <- function() {
}
tryCatch(fn, error = function(e) {})
# Previously became:
tryCatch(fn, error = function(e) {
})
my_fn(expr = {}, option = TRUE)
# Previously became:
my_fn(
expr = {
},
option = TRUE
)
As of 0.7.0, empty braces
{}
are now never expanded, which retains the original form of each of these examples.
skip
configuration
In
our release post, we detailed how to disable formatting using a # fmt: skip
comment for a single expression, or a # fmt: skip file
comment for an entire file. 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:
igraph::graph_from_literal(A +-+ B +---+ C ++ D + E)
By default, Air would format this as:
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
now supports a skip
field 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:
skip = ["graph_from_literal"]
In the short term, you may also want to use this for
tibble::tribble()
calls, i.e. skip = ["tribble"]
. In the long term, we’re hoping to provide more sophisticated tooling for formatting using a
specified alignment.
GitHub Action
Air now has an official GitHub Action,
setup-air
. This action really only has one job - to get Air installed on your GitHub runner and put on the PATH
. The basic usage is:
- name: Install Air
uses: posit-dev/setup-air@v1
If you need to pin a version:
- name: Install Air 0.4.4
uses: posit-dev/setup-air@v1
with:
version: "0.4.4"
From there, you can call Air’s CLI in downstream steps. A minimal workflow that errors if any files require formatting might look like:
- name: Install Air
uses: posit-dev/setup-air@v1
- name: Check formatting
run: air format . --check
Rather than creating the workflow file yourself, we instead recommend using usethis to pull in our example workflow:
usethis::use_github_action(url = "https://github.com/posit-dev/setup-air/blob/main/examples/format-suggest.yaml")
This is a special workflow that runs on pull requests. It calls air format
and then uses
reviewdog/action-suggester
to push any formatting diffs as GitHub Suggestion comments on your pull request. It looks like this:
You can accept all suggestions in a single batch, which will then rerun the format check, along with any other GitHub workflows (like an R package check), so you can feel confident that accepting the changes hasn’t broken anything.
We like this workflow because it provides an easy way for external contributors who aren’t using Air to still abide by your formatting rules. The external contributor can even accept the suggestions themselves, so by the time you look at their pull request it’s already good to go from a formatting perspective ✅!
Acknowledgements
A big thanks to the 49 users who helped make this release possible by finding bugs, discussing issues, contributing documentation, and writing code: @adisarid, @aronatkins, @ateucher, @avhz, @aymennasri, @christophe-gouel, @dkStevensNZed, @eitsupi, @ELICHOS, @fh-mthomson, @fzenoni, @gaborcsardi, @grasshoppermouse, @hadley, @idavydov, @j-dobner, @jacpete, @jeffkeller-einc, @jhk0530, @joakimlinde, @JosephBARBIERDARNAL, @JosiahParry, @kkanden, @krlmlr, @Kupac, @kv9898, @lcolladotor, @lulunac27a, @m-muecke, @maelle, @matanhakim, @njtierney, @novica, @ntluong95, @philibe, @PMassicotte, @RobinKohrs, @salim-b, @sawelch-NIVA, @schochastics, @Sebastian-T-T, @stevenpav-helm, @t-kalinowski, @taplasz, @tbadams45cdm, @wurli, @xx02al, @Yunuuuu, and @yutannihilation.