Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@
^internal$
^revdep-cloud$
^CRAN-SUBMISSION$
^[\.]?air\.toml$
^\.vscode$
^[.]?air[.]toml$
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
"[r]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "Posit.air-vscode"
},
"[quarto]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "quarto.quarto"
}
}
45 changes: 23 additions & 22 deletions R/air.R
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
#' used by the Air extension installed through either VS Code or Positron, see
#' the Installation section for more details. Specifically it:
#'
#' - Sets `editor.formatOnSave = true` for R files to enable formatting on
#' every save.
#' - Sets `editor.formatOnSave = true` for R and Quarto files to enable
#' formatting on every save.
#'
#' - Sets `editor.defaultFormatter` to Air for R files to ensure that Air is
#' always selected as the formatter for this project.
#' always selected as the formatter for this project. Likewise, sets the
#' default formatter for Quarto.
#'
#' - Sets the Air extension as a "recommended" extension for this project,
#' which triggers a notification for contributors coming to this project
Expand All @@ -31,20 +32,21 @@
#' may prefer another editor.
#'
#' Note that `use_air()` does not actually invoke Air, it just configures your
#' project with the recommended settings. Consult the [editors
#' project with the recommended settings. Consult [Air's editors
#' guide](https://posit-dev.github.io/air/editors.html) to learn how to invoke
#' Air in your preferred editor.
#'
#' ## Installation
#'
#' Note that this setup does not install an Air binary, so there is an
#' Note that this setup does not install an Air binary, so there may be an
#' additional manual step you must take before using Air for the first time:
#'
#' - For RStudio, follow the [installation
#' guide](https://posit-dev.github.io/air/editor-rstudio.html).
#'
#' - For Positron, install the [OpenVSX
#' Extension](https://open-vsx.org/extension/posit/air-vscode).
#' - For Positron, the [Air extension](https://open-vsx.org/extension/posit/air-vscode)
#' is installed by default and that already includes the Air binary. A typical
#' Positron user does not need to do anything to install Air.
#'
#' - For VS Code, install the [VS Code
#' Extension](https://marketplace.visualstudio.com/items?itemName=Posit.air-vscode).
Expand Down Expand Up @@ -118,7 +120,7 @@ create_air_toml <- function(ignore = FALSE) {

air_toml_regex <- function() {
# Pre-escaped regex allowing both `air.toml` and `.air.toml`
"^[\\.]?air\\.toml$"
"^[.]?air[.]toml$"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the \\. was purposeful. i.e. we have ^\.vscode$ in our Rbuildignore along with many other \. usage.

Unless the [] does something special, I think we should end up with [\.] as well, otherwise doesn't a lone . mean "anything" or something like that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot tells me they are functionally the same, but \. is probably more common for escaping a literal ., and that seems to align with our existing conventions in .Rbuildignore. Thoughts?

Screenshot 2025-07-10 at 8 41 30 AM

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[] is commonly used as a less heavy quote syntax for single characters. I like it as it's easier to read.

@jennybc jennybc Jul 10, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's perhaps a really small hill to die on, but, yes, I am a big fan of using [] instead of escaping, when possible. (But yes, you'll see some escaping in usethis because history.) I really don't care deeply about this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [] is fine

}

create_vscode_json_file <- function(name) {
Expand All @@ -142,20 +144,19 @@ create_vscode_json_file <- function(name) {
}

write_air_vscode_settings_json <- function(path) {
settings <- jsonlite::read_json(path)
settings_r <- settings[["[r]"]]

if (is.null(settings_r)) {
# Mock it
settings_r <- set_names(list())
}

# Set these regardless of their previous values. Assume that calling
# `use_air()` is an explicit request to opt in to these settings.
settings_r[["editor.formatOnSave"]] <- TRUE
settings_r[["editor.defaultFormatter"]] <- "Posit.air-vscode"

settings[["[r]"]] <- settings_r
Comment on lines -145 to -158

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a little worried modifyList() would overwrite an existing [r] or [quarto] setting that wasn't formatOnSave or defaultFormatter, but I see your new test with

    "[quarto]" = list(
      "editor.wordWrap" = "wordWrapColumn"
    )

And it looks like it preserves that, so that's a win for modifyList() I guess 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really is designed for exactly this.

settings <- jsonlite::read_json(path) %||% set_names(list())

patch <- list(
`[r]` = list(
"editor.formatOnSave" = TRUE,
"editor.defaultFormatter" = "Posit.air-vscode"
),
`[quarto]` = list(
"editor.formatOnSave" = TRUE,
"editor.defaultFormatter" = "quarto.quarto"
)
)
settings <- utils::modifyList(settings, patch)

write_vscode_json(x = settings, path = path)
}
Expand Down
15 changes: 9 additions & 6 deletions man/use_air.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion tests/testthat/_snaps/air.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use_air()
Message
v Creating 'air.toml'.
v Adding "^[\\.]?air\\.toml$" to '.Rbuildignore'.
v Adding "^[.]?air[.]toml$" to '.Rbuildignore'.
v Creating '.vscode/'.
v Adding "^\\.vscode$" to '.Rbuildignore'.
v Creating '.vscode/settings.json'.
Expand All @@ -21,6 +21,10 @@
"[r]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "Posit.air-vscode"
},
"[quarto]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "quarto.quarto"
}
}

Expand Down
22 changes: 17 additions & 5 deletions tests/testthat/test-air.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ test_that("creates correct default package files", {
settings[["[r]"]][["editor.defaultFormatter"]],
"Posit.air-vscode"
)
expect_true(settings[["[quarto]"]][["editor.formatOnSave"]])
expect_identical(
settings[["[quarto]"]][["editor.defaultFormatter"]],
"quarto.quarto"
)

settings <- jsonlite::read_json(proj_path(".vscode", "extensions.json"))
recommendations <- settings[["recommendations"]]
Expand Down Expand Up @@ -48,6 +53,11 @@ test_that("creates correct default project files", {
settings[["[r]"]][["editor.defaultFormatter"]],
"Posit.air-vscode"
)
expect_true(settings[["[quarto]"]][["editor.formatOnSave"]])
expect_identical(
settings[["[quarto]"]][["editor.defaultFormatter"]],
"quarto.quarto"
)

settings <- jsonlite::read_json(proj_path(".vscode", "extensions.json"))
recommendations <- settings[["recommendations"]]
Expand All @@ -68,6 +78,9 @@ test_that("respects existing `settings.json`, but overwrites settings we own", {
),
"[rust]" = list(
"editor.formatOnSave" = FALSE
),
"[quarto]" = list(
"editor.wordWrap" = "wordWrapColumn"
)
)

Expand All @@ -76,11 +89,10 @@ test_that("respects existing `settings.json`, but overwrites settings we own", {
use_air()

# Here is all that should change
settings[["[r]"]] <- list(
# Overwrite both of these to Air's recommendations
"editor.formatOnSave" = TRUE,
"editor.defaultFormatter" = "Posit.air-vscode"
)
settings[["[r]"]][["editor.formatOnSave"]] <- TRUE
settings[["[r]"]][["editor.defaultFormatter"]] <- "Posit.air-vscode"
settings[["[quarto]"]][["editor.formatOnSave"]] <- TRUE
settings[["[quarto]"]][["editor.defaultFormatter"]] <- "quarto.quarto"

actual_settings <- jsonlite::read_json(path)

Expand Down