Skip to content

Commit cb88f0f

Browse files
feat: add use_vscode() (#441)
* feat: add `use_vscode` to write `settings.json` * feat: add internal `is_vscode()` to detect if running in VSCode * chore: add `use_vscode()` to `use_extendr()` conditional to VSCode detected. * tests: add tests for `is_vscode()`and `use_vscode()` * chore: fix lint * tests: update use_extendr snapshot * fdocs: add index to site * feat: add alias `use_positron()` * feat: add is_positron() to detect Positron's exclusive env vars * feat: add files associations to use_vscode()/positron * chore: make more robust Positron detection in use_extendr() * tests: update snapshots * tests: update use_vscode() tests * tests: update snapshots * chore: update NEWS * Update R/use_vscode.R [skip ci] * Update R/use_vscode.R [skip ci] --------- Co-authored-by: Josiah Parry <josiah.parry@gmail.com>
1 parent 12711a3 commit cb88f0f

File tree

10 files changed

+236
-0
lines changed

10 files changed

+236
-0
lines changed

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export(to_toml)
2424
export(use_crate)
2525
export(use_extendr)
2626
export(use_msrv)
27+
export(use_positron)
28+
export(use_vscode)
2729
export(vendor_pkgs)
2830
export(write_license_note)
2931
importFrom(dplyr,mutate)

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# rextendr (development version)
22

3+
* Added `use_vscode()` and its alias `use_positron()` to create `.vscode/settings.json`, enhancing the `rextendr` experience in VSCode/Positron.
4+
Additionally, `use_extendr()` now automatically calls `use_vscode()` when VSCode or Positron is detected as the IDE (#441).
5+
36
# rextendr 0.4.0
47

58
* Adds [WebR](https://docs.r-wasm.org/webr/latest/) support out of the box for all extendr packages.

R/use_extendr.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ use_extendr <- function(
209209
data = list(lib_name = lib_name)
210210
)
211211

212+
# create settings.json file
213+
if (is_vscode() || is_positron()) use_vscode()
214+
212215
# configure needs to be made executable
213216
# ignore for Windows
214217
if (.Platform[["OS.type"]] == "unix") {

R/use_vscode.R

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#' Set up VS Code configuration for an rextendr project
2+
#'
3+
#' @description This creates a `.vscode` folder (if needed) and populates it with a
4+
#' `settings.json` template. If already exists, it will be updated to include
5+
#' the `rust-analyzer.linkedProjects` setting.
6+
#'
7+
#' @param quiet If `TRUE`, suppress messages.
8+
#' @param overwrite If `TRUE`, overwrite existing files.
9+
#' @details Rust-Analyzer VSCode extension looks for a `Cargo.toml` file in the
10+
#' workspace root by default. This function creates a `.vscode` folder and
11+
#' populates it with a `settings.json` file that sets the workspace root to
12+
#' the `src` directory of the package. This allows you to open the package
13+
#' directory in VSCode and have the Rust-Analyzer extension work correctly.
14+
#' @return `TRUE` (invisibly) if the settings file was created or updated.
15+
#' @export
16+
use_vscode <- function(quiet = FALSE, overwrite = FALSE) {
17+
if (!dir.exists(".vscode")) {
18+
dir.create(".vscode")
19+
}
20+
21+
usethis::use_build_ignore(file.path(".vscode"))
22+
23+
settings_path <- file.path(".vscode", "settings.json")
24+
rust_analyzer_path <- "${workspaceFolder}/src/rust/Cargo.toml"
25+
files_associations <- list(
26+
"Makevars.in" = "makefile",
27+
"Makevars.win" = "makefile",
28+
"configure" = "shellscript",
29+
"configure.win" = "shellscript",
30+
"cleanup" = "shellscript",
31+
"cleanup.win" = "shellscript"
32+
)
33+
34+
if (file.exists(settings_path) && !overwrite) {
35+
if (!quiet) message("Updating existing .vscode/settings.json")
36+
37+
# settings.json accepts trailing commas before braces and brackets and {jsonlite} doesn't dig that
38+
tryCatch({
39+
settings <- jsonlite::read_json(settings_path)
40+
}, error = function(e) {
41+
if (grepl("parse error", e$message)) {
42+
stop(
43+
"Could not parse .vscode/settings.json. Do you have a trailing comma before braces or brackets?\n",
44+
"Original error: : ", e$message
45+
)
46+
} else {
47+
stop(e$message)
48+
}
49+
})
50+
51+
# checking and updating cargo.toml path for Rust-Analyzer
52+
if (!"rust-analyzer.linkedProjects" %in% names(settings)) {
53+
settings[["rust-analyzer.linkedProjects"]] <- list(rust_analyzer_path)
54+
} else if (!rust_analyzer_path %in% settings[["rust-analyzer.linkedProjects"]]) {
55+
settings[["rust-analyzer.linkedProjects"]] <- c(
56+
settings[["rust-analyzer.linkedProjects"]],
57+
rust_analyzer_path
58+
)
59+
}
60+
61+
# checking and updating files associations
62+
if (!"files.associations" %in% names(settings)) {
63+
settings[["files.associations"]] <- files_associations
64+
} else {
65+
current_assoc <- settings[["files.associations"]]
66+
for (name in names(files_associations)) {
67+
current_assoc[[name]] <- files_associations[[name]]
68+
}
69+
settings[["files.associations"]] <- current_assoc
70+
}
71+
72+
jsonlite::write_json(
73+
settings,
74+
settings_path,
75+
auto_unbox = TRUE,
76+
pretty = TRUE
77+
)
78+
} else {
79+
use_rextendr_template(
80+
"settings.json",
81+
save_as = settings_path,
82+
quiet = quiet,
83+
overwrite = overwrite
84+
)
85+
}
86+
87+
invisible(TRUE)
88+
}
89+
90+
#' @rdname use_vscode
91+
#' @export
92+
use_positron <- use_vscode

R/utils.R

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,19 @@ is_osx <- function() {
8585
}
8686
grepl("^darwin", R.version$os, ignore.case = TRUE)
8787
}
88+
89+
is_vscode <- function() {
90+
e <- Sys.getenv(c("VSCODE_PID", "VSCODE_CWD", "VSCODE_IPC_HOOK_CLI", "TERM_PROGRAM"))
91+
if (nzchar(e["VSCODE_PID"]) || nzchar(e["VSCODE_CWD"]) || nzchar(e["VSCODE_IPC_HOOK_CLI"]) || tolower(e["TERM_PROGRAM"]) == "vscode") { # nolint
92+
return(TRUE)
93+
}
94+
FALSE
95+
}
96+
97+
is_positron <- function() {
98+
e <- Sys.getenv(c("POSITRON", "POSITRON_LONG_VERSION", "POSITRON_MODE", "POSITRON_VERSION"))
99+
if (nzchar(e["POSITRON"]) || nzchar(e["POSITRON_LONG_VERSION"]) || nzchar(e["POSITRON_MODE"]) || nzchar(e["POSITRON_VERSION"])) { # nolint
100+
return(TRUE)
101+
}
102+
FALSE
103+
}

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ reference:
2525
- cran
2626
- vendor_pkgs
2727
- use_msrv
28+
- use_vscode
2829

2930
- title: Various utility functions
3031
contents:

inst/templates/settings.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"rust-analyzer.linkedProjects": [
3+
"${workspaceFolder}/src/rust/Cargo.toml"
4+
],
5+
"files.associations": {
6+
"Makevars.in": "makefile",
7+
"Makevars.win": "makefile",
8+
"configure": "shellscript",
9+
"configure.win": "shellscript",
10+
"cleanup": "shellscript",
11+
"cleanup.win": "shellscript"
12+
}
13+
}

man/use_vscode.Rd

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-use_vscode.R

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
test_that("use_vscode creates .vscode and writes settings.json", {
2+
skip_if_not_installed("usethis")
3+
4+
path <- local_package("testpkg")
5+
withr::local_options(usethis.quiet = FALSE)
6+
7+
use_vscode(quiet = TRUE, overwrite = TRUE)
8+
9+
expect_true(dir.exists(".vscode"))
10+
settings <- jsonlite::read_json(file.path(".vscode", "settings.json"))
11+
expect_equal(
12+
settings[["rust-analyzer.linkedProjects"]],
13+
list("${workspaceFolder}/src/rust/Cargo.toml")
14+
)
15+
})
16+
17+
test_that("use_vscode is idempotent and does not duplicate entries", {
18+
skip_if_not_installed("usethis")
19+
20+
path <- local_package("testpkg")
21+
withr::local_options(usethis.quiet = FALSE)
22+
23+
use_vscode(quiet = TRUE, overwrite = TRUE)
24+
use_vscode(quiet = TRUE, overwrite = FALSE)
25+
26+
settings <- jsonlite::read_json(file.path(".vscode", "settings.json"))
27+
expect_equal(length(settings[["rust-analyzer.linkedProjects"]]), 1)
28+
})
29+
30+
test_that("overwrite = TRUE replaces existing settings.json", {
31+
skip_if_not_installed("usethis")
32+
33+
path <- local_package("testpkg")
34+
withr::local_options(usethis.quiet = FALSE)
35+
36+
use_vscode(quiet = TRUE, overwrite = TRUE)
37+
# corrupt the file
38+
jsonlite::write_json(list(foo = "bar"), file.path(".vscode", "settings.json"), auto_unbox = TRUE)
39+
use_vscode(quiet = TRUE, overwrite = TRUE)
40+
41+
settings2 <- jsonlite::read_json(file.path(".vscode", "settings.json"))
42+
expect_null(settings2$foo)
43+
expect_equal(
44+
settings2[["rust-analyzer.linkedProjects"]],
45+
list("${workspaceFolder}/src/rust/Cargo.toml")
46+
)
47+
})

tests/testthat/test-utils.R

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,31 @@ test_that("`replace_na()` replaces with the correct value", {
3232
x[2] <- NA_integer_
3333
expect_identical(replace_na(x, -99L), c(1L, -99L, 3L, 4L, 5L))
3434
})
35+
36+
test_that("is_vscode() returns FALSE when VSCode environment variables are not set", {
37+
withr::with_envvar(
38+
c(
39+
VSCODE_PID = "",
40+
VSCODE_CWD = "",
41+
VSCODE_IPC_HOOK_CLI = "",
42+
TERM_PROGRAM = ""
43+
),
44+
{
45+
expect_false(is_vscode())
46+
}
47+
)
48+
})
49+
50+
test_that("is_vscode() returns TRUE when VSCode environment variables are set", {
51+
withr::with_envvar(
52+
c(
53+
VSCODE_PID = "",
54+
VSCODE_CWD = "",
55+
VSCODE_IPC_HOOK_CLI = "",
56+
TERM_PROGRAM = "vscode"
57+
),
58+
{
59+
expect_true(is_vscode())
60+
}
61+
)
62+
})

0 commit comments

Comments
 (0)