|
| 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 | +} |
0 commit comments