Skip to content

Commit 72b1201

Browse files
authored
Air 0.7.0 post (#739)
* Air 0.7.0 post * Add full acknowledgements section * Move side effect discussion to a subsection * Update date
1 parent 399114d commit 72b1201

File tree

6 files changed

+584
-0
lines changed

6 files changed

+584
-0
lines changed
152 KB
Loading

content/blog/air-0-7-0/index.Rmd

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
---
2+
output: hugodown::hugo_document
3+
4+
slug: air-0-7-0
5+
title: Air 0.7.0
6+
date: 2025-06-11
7+
author: Davis Vaughan and Lionel Henry
8+
description: >
9+
Read all about Air 0.7.0, including: even better Positron support, a new feature we call autobracing, and an official GitHub Action!
10+
11+
photo:
12+
url: https://unsplash.com/photos/photo-of-island-and-thunder-E-Zuyev2XWo
13+
author: Johannes Plenio
14+
15+
categories: [programming]
16+
tags: []
17+
18+
editor:
19+
markdown:
20+
wrap: sentence
21+
canonical: true
22+
---
23+
24+
We're very excited to announce [Air 0.7.0](https://posit-dev.github.io/air/), a new release of our extremely fast R formatter.
25+
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!
26+
If you haven't heard of Air, read our [announcement blog post](https://www.tidyverse.org/blog/2025/02/air/) first to get up to speed.
27+
To install Air, read our [editors guide](https://posit-dev.github.io/air/editors.html).
28+
29+
## Positron
30+
31+
The [Air extension](https://open-vsx.org/extension/posit/air-vscode) is now included in [Positron](https://positron.posit.co/) by default, and will automatically keep itself up to date.
32+
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!
33+
Positron now also ships with [Ruff](https://docs.astral.sh/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.
34+
35+
We've also streamlined the process of adding Air to a new or existing project.
36+
With dev usethis, you can now run [`usethis::use_air()`](https://usethis.r-lib.org/dev/reference/use_air.html) to automatically configure recommended Air settings.
37+
In particular, this will:
38+
39+
- Create an [empty `air.toml`](https://posit-dev.github.io/air/configuration.html#configuration-recommendations).
40+
41+
- Create `.vscode/settings.json` filled with the following settings.
42+
This enables `Format on Save` within your workspace.
43+
44+
``` json
45+
{
46+
"[r]": {
47+
"editor.formatOnSave": true,
48+
"editor.defaultFormatter": "Posit.air-vscode"
49+
}
50+
}
51+
```
52+
53+
- Create `.vscode/extensions.json` filled with the following settings.
54+
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!
55+
56+
``` json
57+
{
58+
"recommendations": [
59+
"Posit.air-vscode"
60+
]
61+
}
62+
```
63+
64+
- Update your `.Rbuildignore` to exclude Air related configuration, if you're working on an R package.
65+
66+
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).
67+
I've found that this is invaluable for adopting Air in an existing project!
68+
69+
To summarize, we've reduced our advice on adding Air to an existing project down to:
70+
71+
- Open Positron
72+
73+
- Run `usethis::use_air()`
74+
75+
- Run `Air: Format Workspace Folder`
76+
77+
- Commit, push, and then enjoy using `Format on Save` forevermore 😄
78+
79+
## More editors!
80+
81+
Positron isn't the only editor that's received some love!
82+
We now have official documentation for using Air in the following editors:
83+
84+
- [Zed](https://posit-dev.github.io/air/editor-zed.html)
85+
86+
- [Neovim](https://posit-dev.github.io/air/editor-neovim.html)
87+
88+
- [Helix](https://posit-dev.github.io/air/editor-helix.html)
89+
90+
We're very proud of the fact that Air can be used within any editor, not just RStudio and Positron!
91+
This documentation was a community effort - thanks in particular to [\@taplasz](https://github.com/taplasz), [\@PMassicotte](https://github.com/PMassicotte), [\@m-muecke](https://github.com/m-muecke), [\@TymekDev](https://github.com/TymekDev), and [\@wurli](https://github.com/wurli).
92+
93+
## Autobracing
94+
95+
Autobracing is the process of adding braces (i.e. `{ }`) to if statements, loops, and function definitions to create more consistent, readable, and portable code.
96+
It looks like this:
97+
98+
``` r
99+
for (i in seq_along(x)) x[[i]] <- x[[i]] + 1L
100+
101+
# Becomes:
102+
for (i in seq_along(x)) {
103+
x[[i]] <- x[[i]] + 1L
104+
}
105+
106+
function(x, y)
107+
call_that_spans_lines(
108+
x,
109+
y,
110+
fixed_option = FALSE
111+
)
112+
113+
# Becomes:
114+
function(x, y) {
115+
call_that_spans_lines(
116+
x,
117+
y,
118+
fixed_option = FALSE
119+
)
120+
}
121+
```
122+
123+
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.
124+
Consider the following if statement:
125+
126+
``` r
127+
do_something <- function(this = TRUE) {
128+
if (this)
129+
do_this()
130+
else
131+
do_that()
132+
}
133+
```
134+
135+
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:
136+
137+
``` r
138+
if (this)
139+
do_this()
140+
else
141+
do_that()
142+
#> Error: unexpected 'else'
143+
```
144+
145+
In practice, this typically bites you when you're debugging and you send a chunk of lines to the console:
146+
147+
<video controls autoplay loop muted width="100%" src="video/portable-if-statement.mov" style="border: 2px solid #CCC;">
148+
149+
</video>
150+
151+
Air autobraces this if statement to the following, which has no issues with portability:
152+
153+
``` r
154+
do_something <- function(this = TRUE) {
155+
if (this) {
156+
do_this()
157+
} else {
158+
do_that()
159+
}
160+
}
161+
```
162+
163+
### Give side effects some Air
164+
165+
We believe code that create *side effects* which modify state or affect control flow are important enough to live on their own line.
166+
For example, the following `stop()` call is an example of a side effect, so it moves to its own line and is autobraced:
167+
168+
``` r
169+
if (anyNA(x)) stop("`x` can't contain missing values.")
170+
171+
# Becomes:
172+
if (anyNA(x)) {
173+
stop("`x` can't contain missing values.")
174+
}
175+
```
176+
177+
You might be thinking, "But I like my single line if statements!" We do too!
178+
Air still allows single line if statements if they look to be used for their *value* rather than for their *side effect*.
179+
These single line if statements are still allowed:
180+
181+
``` r
182+
x <- if (condition) this else that
183+
184+
x <- x %||% if (condition) this else that
185+
186+
list(a = if (condition) this else that)
187+
```
188+
189+
Similarly, single line function definitions are also still allowed if they don't already have braces and don't exceed the line length:
190+
191+
``` r
192+
add_one <- function(x) x + 1
193+
194+
bools <- map_lgl(xs, function(x) is.logical(x) && length(x) == 1L && !is.na(x))
195+
```
196+
197+
For the full set of rules, check out our [documentation on autobracing](https://posit-dev.github.io/air/formatter.html#autobracing).
198+
199+
## Empty braces
200+
201+
You may have noticed the following forced expansion of empty `{}` in previous versions of Air:
202+
203+
``` r
204+
dummy <- function() {}
205+
206+
# Previously became:
207+
dummy <- function() {
208+
}
209+
210+
tryCatch(fn, error = function(e) {})
211+
212+
# Previously became:
213+
tryCatch(fn, error = function(e) {
214+
})
215+
216+
my_fn(expr = {}, option = TRUE)
217+
218+
# Previously became:
219+
my_fn(
220+
expr = {
221+
},
222+
option = TRUE
223+
)
224+
```
225+
226+
As of 0.7.0, empty braces `{}` are now never expanded, which retains the original form of each of these examples.
227+
228+
## `skip` configuration
229+
230+
In [our release post](https://www.tidyverse.org/blog/2025/02/air/#how-can-i-disable-formatting), 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.
231+
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.
232+
For example, the igraph package contains a DSL for constructing a graph from a literal representation:
233+
234+
``` r
235+
igraph::graph_from_literal(A +-+ B +---+ C ++ D + E)
236+
```
237+
238+
By default, Air would format this as:
239+
240+
``` r
241+
igraph::graph_from_literal(A + -+B + ---+C + +D + E)
242+
```
243+
244+
If you use `graph_from_literal()` often, it would be annoying to add `# fmt: skip` comments at every call site.
245+
Instead, `air.toml` now supports a `skip` field that allows you to specify function names that you never want formatting for.
246+
Specifying this would retain the original formatting of the `graph_from_literal()` call, even without a `# fmt: skip` comment:
247+
248+
``` toml
249+
skip = ["graph_from_literal"]
250+
```
251+
252+
In the short term, you may also want to use this for `tibble::tribble()` calls, i.e. `skip = ["tribble"]`.
253+
In the long term, we're hoping to provide more sophisticated tooling for formatting using a [specified alignment](https://github.com/posit-dev/air/issues/113).
254+
255+
## GitHub Action
256+
257+
Air now has an official GitHub Action, [`setup-air`](https://github.com/posit-dev/setup-air).
258+
This action really only has one job - to get Air installed on your GitHub runner and put on the `PATH`.
259+
The basic usage is:
260+
261+
``` yaml
262+
- name: Install Air
263+
uses: posit-dev/setup-air@v1
264+
```
265+
266+
If you need to pin a version:
267+
268+
``` yaml
269+
- name: Install Air 0.4.4
270+
uses: posit-dev/setup-air@v1
271+
with:
272+
version: "0.4.4"
273+
```
274+
275+
From there, you can call Air's CLI in downstream steps.
276+
A minimal workflow that errors if any files require formatting might look like:
277+
278+
``` yaml
279+
- name: Install Air
280+
uses: posit-dev/setup-air@v1
281+
282+
- name: Check formatting
283+
run: air format . --check
284+
```
285+
286+
Rather than creating the workflow file yourself, we instead recommend using usethis to pull in our [example workflow](https://github.com/posit-dev/setup-air/blob/main/examples/format-suggest.yaml):
287+
288+
``` r
289+
usethis::use_github_action(url = "https://github.com/posit-dev/setup-air/blob/main/examples/format-suggest.yaml")
290+
```
291+
292+
This is a special workflow that runs on pull requests.
293+
It calls `air format` and then uses [`reviewdog/action-suggester`](https://github.com/reviewdog/action-suggester) to push any formatting diffs as GitHub Suggestion comments on your pull request.
294+
It looks like this:
295+
296+
![](figs/format-suggest-example.png)
297+
298+
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.
299+
300+
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.
301+
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 ✅!
302+
303+
## Acknowledgements
304+
305+
A big thanks to the 49 users who helped make this release possible by finding bugs, discussing issues, contributing documentation, and writing code: [\@adisarid](https://github.com/adisarid), [\@aronatkins](https://github.com/aronatkins), [\@ateucher](https://github.com/ateucher), [\@avhz](https://github.com/avhz), [\@aymennasri](https://github.com/aymennasri), [\@christophe-gouel](https://github.com/christophe-gouel), [\@dkStevensNZed](https://github.com/dkStevensNZed), [\@eitsupi](https://github.com/eitsupi), [\@ELICHOS](https://github.com/ELICHOS), [\@fh-mthomson](https://github.com/fh-mthomson), [\@fzenoni](https://github.com/fzenoni), [\@gaborcsardi](https://github.com/gaborcsardi), [\@grasshoppermouse](https://github.com/grasshoppermouse), [\@hadley](https://github.com/hadley), [\@idavydov](https://github.com/idavydov), [\@j-dobner](https://github.com/j-dobner), [\@jacpete](https://github.com/jacpete), [\@jeffkeller-einc](https://github.com/jeffkeller-einc), [\@jhk0530](https://github.com/jhk0530), [\@joakimlinde](https://github.com/joakimlinde), [\@JosephBARBIERDARNAL](https://github.com/JosephBARBIERDARNAL), [\@JosiahParry](https://github.com/JosiahParry), [\@kkanden](https://github.com/kkanden), [\@krlmlr](https://github.com/krlmlr), [\@Kupac](https://github.com/Kupac), [\@kv9898](https://github.com/kv9898), [\@lcolladotor](https://github.com/lcolladotor), [\@lulunac27a](https://github.com/lulunac27a), [\@m-muecke](https://github.com/m-muecke), [\@maelle](https://github.com/maelle), [\@matanhakim](https://github.com/matanhakim), [\@njtierney](https://github.com/njtierney), [\@novica](https://github.com/novica), [\@ntluong95](https://github.com/ntluong95), [\@philibe](https://github.com/philibe), [\@PMassicotte](https://github.com/PMassicotte), [\@RobinKohrs](https://github.com/RobinKohrs), [\@salim-b](https://github.com/salim-b), [\@sawelch-NIVA](https://github.com/sawelch-NIVA), [\@schochastics](https://github.com/schochastics), [\@Sebastian-T-T](https://github.com/Sebastian-T-T), [\@stevenpav-helm](https://github.com/stevenpav-helm), [\@t-kalinowski](https://github.com/t-kalinowski), [\@taplasz](https://github.com/taplasz), [\@tbadams45cdm](https://github.com/tbadams45cdm), [\@wurli](https://github.com/wurli), [\@xx02al](https://github.com/xx02al), [\@Yunuuuu](https://github.com/Yunuuuu), and [\@yutannihilation](https://github.com/yutannihilation).

0 commit comments

Comments
 (0)