|
| 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 | + |
| 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