Skip to content

Commit a653d6e

Browse files
Add use_air() (#2109)
* Add `use_air()` * NEWS bullet * Add to `_pkgdown.yml` * Note that these do nothing if the setting was already set * Add `@noRd` and installation section * Remove `dot_prefix` option * Rework `create_air_toml()` to use `path_first_existing()` and only message when actually creating the file * Only message about `settings.json` and `extensions.json` when we actually create them * Use `expect_proj_file()` * Use `writeLines()` * Mention the extension in the main bullet * Set `editor.formatOnSave` and `editor.defaultFormatter` unconditionally * Add a todo to read how to invoke Air * Use github pages URL after all - because we already reference it elsewhere * Line length * Add a snapshot for typical 1st invocation --------- Co-authored-by: Jenny Bryan <[email protected]>
1 parent 8769507 commit a653d6e

File tree

9 files changed

+480
-1
lines changed

9 files changed

+480
-1
lines changed

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export(ui_yeah)
8383
export(use_addin)
8484
export(use_agpl3_license)
8585
export(use_agpl_license)
86+
export(use_air)
8687
export(use_apache_license)
8788
export(use_apl2_license)
8889
export(use_article)

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# usethis (development version)
22

3+
* `use_air()` is a new function to configure a project to use
4+
[Air](https://posit-dev.github.io/air), an extremely fast R code formatter.
5+
36
# usethis 3.1.0
47

58
* `use_vignette()` and `use_article()` support Quarto. The `name` of the new

R/air.R

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
#' Configure a project to use Air
2+
#'
3+
#' @description
4+
#' [Air](https://posit-dev.github.io/air) is an extremely fast R code
5+
#' formatter. This function sets up a project to use Air. Specifically, it:
6+
#'
7+
#' - Creates an empty `air.toml` configuration file. If either an `air.toml` or
8+
#' `.air.toml` file already existed, nothing is changed. If the project is an
9+
#' R package, `.Rbuildignore` is updated to ignore this file.
10+
#'
11+
#' - Creates a `.vscode/` directory and adds recommended settings to
12+
#' `.vscode/settings.json` and `.vscode/extensions.json`. These settings are
13+
#' used by the Air extension installed through either VS Code or Positron, see
14+
#' the Installation section for more details. Specifically it:
15+
#'
16+
#' - Sets `editor.formatOnSave = true` for R files to enable formatting on
17+
#' every save.
18+
#'
19+
#' - Sets `editor.defaultFormatter` to Air for R files to ensure that Air is
20+
#' always selected as the formatter for this project.
21+
#'
22+
#' - Sets the Air extension as a "recommended" extension for this project,
23+
#' which triggers a notification for contributors coming to this project
24+
#' that don't yet have the Air extension installed.
25+
#'
26+
#' If the project is an R package, `.Rbuildignore` is updated to ignore the
27+
#' `.vscode/` directory.
28+
#'
29+
#' If you'd like to opt out of VS Code / Positron specific setup, set `vscode
30+
#' = FALSE`, but remember that even if you work in RStudio, other contributors
31+
#' may prefer another editor.
32+
#'
33+
#' Note that `use_air()` does not actually invoke Air, it just configures your
34+
#' project with the recommended settings. Consult the [editors
35+
#' guide](https://posit-dev.github.io/air/editors.html) to learn how to invoke
36+
#' Air in your preferred editor.
37+
#'
38+
#' ## Installation
39+
#'
40+
#' Note that this setup does not install an Air binary, so there is an
41+
#' additional manual step you must take before using Air for the first time:
42+
#'
43+
#' - For RStudio, follow the [installation
44+
#' guide](https://posit-dev.github.io/air/editor-rstudio.html).
45+
#'
46+
#' - For Positron, install the [OpenVSX
47+
#' Extension](https://open-vsx.org/extension/posit/air-vscode).
48+
#'
49+
#' - For VS Code, install the [VS Code
50+
#' Extension](https://marketplace.visualstudio.com/items?itemName=Posit.air-vscode).
51+
#'
52+
#' - For other editors, check to [see if that editor is
53+
#' supported](https://posit-dev.github.io/air/editors.html) by Air.
54+
#'
55+
#' @param vscode Either:
56+
#' - `TRUE` to set up VS Code and Positron specific Air settings. This is the
57+
#' default.
58+
#' - `FALSE` to opt out of those settings.
59+
#'
60+
#' @export
61+
#' @examples
62+
#' \dontrun{
63+
#' # Prepare an R package or project to use Air
64+
#' use_air()
65+
#' }
66+
use_air <- function(vscode = TRUE) {
67+
check_bool(vscode)
68+
69+
ignore <- is_package()
70+
71+
# Create empty `air.toml` if it doesn't exist
72+
create_air_toml(ignore = ignore)
73+
74+
if (vscode) {
75+
create_vscode_directory(ignore = ignore)
76+
77+
# Create project level `settings.json` if it doesn't exist,
78+
# and write in Air specific formatter settings
79+
path <- create_vscode_json_file("settings.json")
80+
write_air_vscode_settings_json(path)
81+
82+
# Create project level `extensions.json` if it doesn't exist,
83+
# and write in Air as a recommended extension for this project
84+
path <- create_vscode_json_file("extensions.json")
85+
write_air_vscode_extensions_json(path)
86+
}
87+
88+
ui_bullets(c(
89+
"_" = "Read the {.href [Air editors guide](https://posit-dev.github.io/air/editors.html)}
90+
to learn how to invoke Air in your preferred editor."
91+
))
92+
93+
invisible(TRUE)
94+
}
95+
96+
#' Creates an empty `air.toml`
97+
#'
98+
#' If either `air.toml` or `.air.toml` already exist, no new file is created.
99+
#'
100+
#' @keywords internal
101+
#' @noRd
102+
create_air_toml <- function(ignore = FALSE) {
103+
path <- path_first_existing(proj_path(c("air.toml", ".air.toml")))
104+
105+
if (is.null(path)) {
106+
# No pre-existing configuration file, create it
107+
path <- proj_path("air.toml")
108+
file_create(path)
109+
ui_bullets(c("v" = "Creating {.path {pth(path)}}."))
110+
}
111+
112+
if (ignore) {
113+
use_build_ignore(air_toml_regex(), escape = FALSE)
114+
}
115+
116+
invisible(path)
117+
}
118+
119+
air_toml_regex <- function() {
120+
# Pre-escaped regex allowing both `air.toml` and `.air.toml`
121+
"^[\\.]?air\\.toml$"
122+
}
123+
124+
create_vscode_json_file <- function(name) {
125+
arg_match(name, values = c("settings.json", "extensions.json"))
126+
127+
path <- proj_path(".vscode", name)
128+
129+
if (!file_exists(path)) {
130+
file_create(path)
131+
ui_bullets(c("v" = "Creating {.path {pth(path)}}."))
132+
}
133+
134+
# Tools like jsonlite fail to read empty json files,
135+
# so if we've just created it, write in `{}`. The easiest
136+
# way to do that is to write an empty named list.
137+
if (is_file_empty(path)) {
138+
jsonlite::write_json(set_names(list()), path = path, pretty = TRUE)
139+
}
140+
141+
invisible(path)
142+
}
143+
144+
write_air_vscode_settings_json <- function(path) {
145+
settings <- jsonlite::read_json(path)
146+
settings_r <- settings[["[r]"]]
147+
148+
if (is.null(settings_r)) {
149+
# Mock it
150+
settings_r <- set_names(list())
151+
}
152+
153+
# Set these regardless of their previous values. Assume that calling
154+
# `use_air()` is an explicit request to opt in to these settings.
155+
settings_r[["editor.formatOnSave"]] <- TRUE
156+
settings_r[["editor.defaultFormatter"]] <- "Posit.air-vscode"
157+
158+
settings[["[r]"]] <- settings_r
159+
160+
write_vscode_json(x = settings, path = path)
161+
}
162+
163+
write_air_vscode_extensions_json <- function(path) {
164+
settings <- jsonlite::read_json(path)
165+
settings_recommendations <- settings[["recommendations"]]
166+
167+
if (is.null(settings_recommendations)) {
168+
# Mock it
169+
settings_recommendations <- list()
170+
}
171+
172+
already_recommended <- any(map_lgl(
173+
settings_recommendations,
174+
function(recommendation) {
175+
identical(recommendation, "Posit.air-vscode")
176+
}
177+
))
178+
179+
if (!already_recommended) {
180+
settings_recommendations <- c(
181+
settings_recommendations,
182+
list("Posit.air-vscode")
183+
)
184+
}
185+
186+
settings[["recommendations"]] <- settings_recommendations
187+
188+
write_vscode_json(x = settings, path = path)
189+
}
190+
191+
#' Write JSON to a VS Code settings file
192+
#'
193+
#' @description
194+
#' Small shim to use in place of [jsonlite::write_json()] when writing to
195+
#' `.vscode/settings.json` or `.vscode/extensions.json`.
196+
#'
197+
#' Notably:
198+
#'
199+
#' - 4 space indent, as that is the standard indent level for these files
200+
#'
201+
#' - Auto unbox, because we want `TRUE` to show up as `true` not `[true]`.
202+
#'
203+
#' - Trims newlines from the right hand side after the ending `}`. Unfortunately
204+
#' setting `pretty = 4L` causes the special libyajl formatter to kick in, and
205+
#' that always adds a trailing newline after every `]` or `}`, even the last
206+
#' one, which we don't want.
207+
#'
208+
#' @keywords internal
209+
#' @noRd
210+
write_vscode_json <- function(x, path) {
211+
json <- jsonlite::toJSON(x, pretty = 4L, auto_unbox = TRUE)
212+
json <- base::trimws(json, which = "right")
213+
base::writeLines(json, path, useBytes = TRUE)
214+
}

R/vscode.R

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# unexported function we are experimenting with
22
use_vscode_debug <- function(open = rlang::is_interactive()) {
3-
usethis::use_directory(".vscode", ignore = TRUE)
3+
create_vscode_directory(ignore = TRUE)
44

55
deps <- proj_deps()
66
lt_pkgs <- deps$package[deps$type == "LinkingTo"]
@@ -41,3 +41,7 @@ use_vscode_debug <- function(open = rlang::is_interactive()) {
4141

4242
invisible(TRUE)
4343
}
44+
45+
create_vscode_directory <- function(ignore = FALSE) {
46+
use_directory(".vscode", ignore = ignore)
47+
}

_pkgdown.yml

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ reference:
8282
- use_lifecycle
8383
- use_standalone
8484
- use_testthat
85+
- use_air
8586
- title: Package release
8687
contents:
8788
- use_cran_comments

inst/WORDLIST

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ LinkingTo
4040
METACRAN
4141
Makefile
4242
ORCID
43+
OpenVSX
4344
PATs
4445
PBC
4546
PRs
@@ -103,6 +104,7 @@ favour
103104
fiascos
104105
filenaming
105106
foofy
107+
formatter
106108
formidabel
107109
frontmatter
108110
fs
@@ -131,6 +133,7 @@ labelled
131133
labelling
132134
learnr
133135
libgit
136+
libyajl
134137
lifecycle
135138
ly
136139
macbook

man/use_air.Rd

+65
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/air.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# creates correct default package files
2+
3+
Code
4+
use_air()
5+
Message
6+
v Creating 'air.toml'.
7+
v Adding "^[\\.]?air\\.toml$" to '.Rbuildignore'.
8+
v Creating '.vscode/'.
9+
v Adding "^\\.vscode$" to '.Rbuildignore'.
10+
v Creating '.vscode/settings.json'.
11+
v Creating '.vscode/extensions.json'.
12+
[ ] Read the Air editors guide (<https://posit-dev.github.io/air/editors.html>)
13+
to learn how to invoke Air in your preferred editor.
14+
15+
---
16+
17+
Code
18+
writeLines(read_utf8(proj_path(".vscode", "settings.json")))
19+
Output
20+
{
21+
"[r]": {
22+
"editor.formatOnSave": true,
23+
"editor.defaultFormatter": "Posit.air-vscode"
24+
}
25+
}
26+
27+
---
28+
29+
Code
30+
writeLines(read_utf8(proj_path(".vscode", "extensions.json")))
31+
Output
32+
{
33+
"recommendations": [
34+
"Posit.air-vscode"
35+
]
36+
}
37+

0 commit comments

Comments
 (0)