From df59ee793bbb8ca235a3d89fb9ea9dbec7dee9c1 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 19 Sep 2025 15:52:38 -0400 Subject: [PATCH 1/8] Add `use_snapshot()` Implement a `use_snapshot()` helper to go with `use_r()` and `use_test()`. I ran out of time and didn't implement tests or NEWS for this yet (and there's a solid chance that this doesn't quite nail it), but I wanted to get it checked in. Fixes #2156. --- NAMESPACE | 1 + R/r.R | 38 +++++++++++++++++++++++++++++++++++++- man/use_snapshot.Rd | 20 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 man/use_snapshot.Rd diff --git a/NAMESPACE b/NAMESPACE index 91a15d28e..dd0b8bdc5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -166,6 +166,7 @@ export(use_rmarkdown_template) export(use_roxygen_md) export(use_rstudio) export(use_rstudio_preferences) +export(use_snapshot) export(use_spell_check) export(use_standalone) export(use_template) diff --git a/R/r.R b/R/r.R index 5b865ee83..2477b7d80 100644 --- a/R/r.R +++ b/R/r.R @@ -88,6 +88,39 @@ use_test <- function(name = NULL, open = rlang::is_interactive()) { invisible(TRUE) } +#' Edit the snapshot file associated with an R or test file +#' +#' This function opens the snapshot file associated with an R or test file, if +#' one exists. +#' +#' @inheritParams use_r +#' @param variant An optional subdirectory within the `_snaps/` directory. +#' @export +use_snapshot <- function( + name = NULL, + variant = NULL, + open = rlang::is_interactive() +) { + if (!uses_testthat()) { + use_testthat_impl() + } + + path_root <- path("tests", "testthat", "_snaps") + if (!is.null(variant)) { + path_root <- path(path_root, variant) + } + # I can't pass "md" to `compute_name()` because we're fine with them giving us + # "R" as the extension here. + path <- path(path_root, basename(compute_name(name)), ext = "md") + + if (!file_exists(path)) { + cli::cli_abort("No snapshot file exists for {.arg {name}}.") + } + edit_file(proj_path(path), open = open) + + invisible(TRUE) +} + #' Create or edit a test helper file #' #' This function creates (or opens) a test helper file, typically @@ -183,7 +216,10 @@ compute_active_name <- function(path, ext, error_call = caller_env()) { path <- proj_path_prep(path_expand_r(path)) dir <- path_dir(proj_rel_path(path)) - if (!dir %in% c("R", "src", "tests/testthat", "tests/testthat/_snaps")) { + if ( + !dir %in% c("R", "src", "tests/testthat", "tests/testthat/_snaps") && + !grepl("tests/testthat/_snaps", dir) + ) { cli::cli_abort( "Open file must be code, test, or snapshot.", call = error_call diff --git a/man/use_snapshot.Rd b/man/use_snapshot.Rd new file mode 100644 index 000000000..b9e1e4f76 --- /dev/null +++ b/man/use_snapshot.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/r.R +\name{use_snapshot} +\alias{use_snapshot} +\title{Edit the snapshot file associated with an R or test file} +\usage{ +use_snapshot(name = NULL, variant = NULL, open = rlang::is_interactive()) +} +\arguments{ +\item{name}{Either a string giving a file name (without directory) or +\code{NULL} to take the name from the currently open file in RStudio.} + +\item{variant}{An optional subdirectory within the \verb{_snaps/} directory.} + +\item{open}{Whether to open the file for interactive editing.} +} +\description{ +This function opens the snapshot file associated with an R or test file, if +one exists. +} From 8ab931d7f446aaf9aa00f5ec3fd4e57cc86d7f02 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Sat, 20 Sep 2025 07:34:55 -0400 Subject: [PATCH 2/8] Fix path calculation and remove basename usage. --- R/r.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/r.R b/R/r.R index 2477b7d80..1c8264ad3 100644 --- a/R/r.R +++ b/R/r.R @@ -111,7 +111,7 @@ use_snapshot <- function( } # I can't pass "md" to `compute_name()` because we're fine with them giving us # "R" as the extension here. - path <- path(path_root, basename(compute_name(name)), ext = "md") + path <- path(path_root, fs::path_ext_set(compute_name(NULL), "md")) if (!file_exists(path)) { cli::cli_abort("No snapshot file exists for {.arg {name}}.") From fa71a445e89b024b98953f71309207f21e4c862f Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Sat, 20 Sep 2025 08:04:04 -0400 Subject: [PATCH 3/8] Add tests and NEWS. --- NEWS.md | 1 + R/r.R | 6 +++--- tests/testthat/_snaps/r.md | 8 ++++++++ tests/testthat/test-r.R | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 37d7ede9f..54295b987 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # usethis (development version) * `pr_resume()` (without a specific `branch`) and `pr_fetch()` (without a specific `number`) no longer error when a branch name contains curly braces (#2107, @jonthegeek). +* `use_snapshot()` is a new function to open a [testthat snapshot file]() corresponding to a given test file or R file (#2156, @jonthegeek). # usethis 3.2.1 diff --git a/R/r.R b/R/r.R index 1c8264ad3..59884d683 100644 --- a/R/r.R +++ b/R/r.R @@ -105,18 +105,18 @@ use_snapshot <- function( use_testthat_impl() } - path_root <- path("tests", "testthat", "_snaps") + path_root <- proj_path("tests", "testthat", "_snaps") if (!is.null(variant)) { path_root <- path(path_root, variant) } # I can't pass "md" to `compute_name()` because we're fine with them giving us # "R" as the extension here. - path <- path(path_root, fs::path_ext_set(compute_name(NULL), "md")) + path <- path(path_root, fs::path_ext_set(compute_name(name), "md")) if (!file_exists(path)) { cli::cli_abort("No snapshot file exists for {.arg {name}}.") } - edit_file(proj_path(path), open = open) + edit_file(path, open = open) invisible(TRUE) } diff --git a/tests/testthat/_snaps/r.md b/tests/testthat/_snaps/r.md index f202815b0..61c6f66b4 100644 --- a/tests/testthat/_snaps/r.md +++ b/tests/testthat/_snaps/r.md @@ -1,3 +1,11 @@ +# use_snapshot() errors for non-existent snapshot file + + Code + use_snapshot("foo", open = FALSE) + Condition + Error in `use_snapshot()`: + ! No snapshot file exists for `foo`. + # use_test_helper() creates a helper file Code diff --git a/tests/testthat/test-r.R b/tests/testthat/test-r.R index 6839f088f..37059b36b 100644 --- a/tests/testthat/test-r.R +++ b/tests/testthat/test-r.R @@ -10,6 +10,24 @@ test_that("use_test() creates a test file", { expect_proj_file("tests", "testthat", "test-foo.R") }) +test_that("use_snapshot() errors for non-existent snapshot file", { + create_local_package() + expect_snapshot( + error = TRUE, + use_snapshot("foo", open = FALSE) + ) +}) + +test_that("use_snapshot() works for existing snapshot file", { + create_local_package() + path <- proj_path("tests", "testthat", "_snaps", "foo.md") + use_directory(path("tests", "testthat", "_snaps")) + write_utf8(path, "## Snapshot for foo") + expect_no_error( + use_snapshot("foo", open = FALSE) + ) +}) + test_that("use_test_helper() creates a helper file", { create_local_package() From 9afc67eb059c13f18019a4554fc8a1a6248aa256 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Sat, 20 Sep 2025 08:09:33 -0400 Subject: [PATCH 4/8] Explain extra clause in active file if. --- R/r.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/r.R b/R/r.R index 59884d683..1cfbde5c9 100644 --- a/R/r.R +++ b/R/r.R @@ -218,6 +218,7 @@ compute_active_name <- function(path, ext, error_call = caller_env()) { dir <- path_dir(proj_rel_path(path)) if ( !dir %in% c("R", "src", "tests/testthat", "tests/testthat/_snaps") && + # This makes sure variants are also supported. !grepl("tests/testthat/_snaps", dir) ) { cli::cli_abort( From ee9dc282769127b9e46b3815828ab949daa4ebf1 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Sat, 20 Sep 2025 08:27:45 -0400 Subject: [PATCH 5/8] Add `use_snapshot` to `_pkgdown.yml` --- _pkgdown.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 6055e2218..63b525161 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -56,6 +56,7 @@ reference: - use_import_from - use_r - use_rmarkdown_template + - use_snapshot - use_spell_check - use_test - use_test_helper From 7ae4acc39051fdaacb1596dbabc0f4e6d918a907 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Fri, 26 Sep 2025 15:59:33 -0500 Subject: [PATCH 6/8] Apply review suggestions. --- R/r.R | 13 +++---------- man/use_snapshot.Rd | 4 +--- tests/testthat/_snaps/r.md | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/R/r.R b/R/r.R index 1cfbde5c9..29d28ef85 100644 --- a/R/r.R +++ b/R/r.R @@ -94,27 +94,20 @@ use_test <- function(name = NULL, open = rlang::is_interactive()) { #' one exists. #' #' @inheritParams use_r -#' @param variant An optional subdirectory within the `_snaps/` directory. #' @export use_snapshot <- function( name = NULL, - variant = NULL, open = rlang::is_interactive() ) { if (!uses_testthat()) { use_testthat_impl() } - path_root <- proj_path("tests", "testthat", "_snaps") - if (!is.null(variant)) { - path_root <- path(path_root, variant) - } - # I can't pass "md" to `compute_name()` because we're fine with them giving us - # "R" as the extension here. - path <- path(path_root, fs::path_ext_set(compute_name(name), "md")) + snap_name <- compute_name(name, ext = "md") + path <- proj_path("tests", "testthat", "_snaps", snap_name) if (!file_exists(path)) { - cli::cli_abort("No snapshot file exists for {.arg {name}}.") + cli::cli_abort("No snapshot file exists for {.var {snap_name}}.") } edit_file(path, open = open) diff --git a/man/use_snapshot.Rd b/man/use_snapshot.Rd index b9e1e4f76..ee018be9c 100644 --- a/man/use_snapshot.Rd +++ b/man/use_snapshot.Rd @@ -4,14 +4,12 @@ \alias{use_snapshot} \title{Edit the snapshot file associated with an R or test file} \usage{ -use_snapshot(name = NULL, variant = NULL, open = rlang::is_interactive()) +use_snapshot(name = NULL, open = rlang::is_interactive()) } \arguments{ \item{name}{Either a string giving a file name (without directory) or \code{NULL} to take the name from the currently open file in RStudio.} -\item{variant}{An optional subdirectory within the \verb{_snaps/} directory.} - \item{open}{Whether to open the file for interactive editing.} } \description{ diff --git a/tests/testthat/_snaps/r.md b/tests/testthat/_snaps/r.md index 61c6f66b4..925a0f9f1 100644 --- a/tests/testthat/_snaps/r.md +++ b/tests/testthat/_snaps/r.md @@ -4,7 +4,7 @@ use_snapshot("foo", open = FALSE) Condition Error in `use_snapshot()`: - ! No snapshot file exists for `foo`. + ! No snapshot file exists for `foo.md`. # use_test_helper() creates a helper file From dca0fdb96df19cb0695fc26c00a824463d73fcc1 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 2 Feb 2026 16:05:51 -0600 Subject: [PATCH 7/8] Combine docs & update --- R/r.R | 26 +++++++++++--------------- man/use_r.Rd | 21 ++++++++++++--------- man/use_rcpp.Rd | 2 +- man/use_snapshot.Rd | 18 ------------------ 4 files changed, 24 insertions(+), 43 deletions(-) delete mode 100644 man/use_snapshot.Rd diff --git a/R/r.R b/R/r.R index 29d28ef85..c9c04e25c 100644 --- a/R/r.R +++ b/R/r.R @@ -1,13 +1,14 @@ -#' Create or edit R or test files +#' Create or edit R, test, and snapshot files #' -#' This pair of functions makes it easy to create paired R and test files, +#' @description +#' This family of functions makes it easy to create paired R and test files, #' using the convention that the tests for `R/foofy.R` should live -#' in `tests/testthat/test-foofy.R`. You can use them to create new files -#' from scratch by supplying `name`, or if you use RStudio, you can call -#' to create (or navigate to) the companion file based on the currently open -#' file. This also works when a test snapshot file is active, i.e. if you're -#' looking at `tests/testthat/_snaps/foofy.md`, `use_r()` or `use_test()` take -#' you to `R/foofy.R` or `tests/testthat/test-foofy.R`, respectively. +#' in `tests/testthat/test-foofy.R` and the results of any snapshot tests live +#' in `tests/testthat/snaps/foofy.md`. +#' +#' You can use them to create new files from scratch by supplying `name`, or +#' if you use RStudio or Positron, you can call to create (or navigate to) the +#' companion file based on the currently open file. #' #' @section Renaming files in an existing package: #' @@ -41,7 +42,7 @@ #' The [rename_files()] function can also be helpful. #' #' @param name Either a string giving a file name (without directory) or -#' `NULL` to take the name from the currently open file in RStudio. +#' `NULL` to take the name from the currently open file in RStudio/Positron. #' @inheritParams edit_file #' @seealso #' * The [testing](https://r-pkgs.org/testing-basics.html) and @@ -88,12 +89,7 @@ use_test <- function(name = NULL, open = rlang::is_interactive()) { invisible(TRUE) } -#' Edit the snapshot file associated with an R or test file -#' -#' This function opens the snapshot file associated with an R or test file, if -#' one exists. -#' -#' @inheritParams use_r +#' @rdname use_r #' @export use_snapshot <- function( name = NULL, diff --git a/man/use_r.Rd b/man/use_r.Rd index 803375f32..ecef03983 100644 --- a/man/use_r.Rd +++ b/man/use_r.Rd @@ -3,27 +3,30 @@ \name{use_r} \alias{use_r} \alias{use_test} -\title{Create or edit R or test files} +\alias{use_snapshot} +\title{Create or edit R, test, and snapshot files} \usage{ use_r(name = NULL, open = rlang::is_interactive()) use_test(name = NULL, open = rlang::is_interactive()) + +use_snapshot(name = NULL, open = rlang::is_interactive()) } \arguments{ \item{name}{Either a string giving a file name (without directory) or -\code{NULL} to take the name from the currently open file in RStudio.} +\code{NULL} to take the name from the currently open file in RStudio/Positron.} \item{open}{Whether to open the file for interactive editing.} } \description{ -This pair of functions makes it easy to create paired R and test files, +This family of functions makes it easy to create paired R and test files, using the convention that the tests for \code{R/foofy.R} should live -in \code{tests/testthat/test-foofy.R}. You can use them to create new files -from scratch by supplying \code{name}, or if you use RStudio, you can call -to create (or navigate to) the companion file based on the currently open -file. This also works when a test snapshot file is active, i.e. if you're -looking at \verb{tests/testthat/_snaps/foofy.md}, \code{use_r()} or \code{use_test()} take -you to \code{R/foofy.R} or \code{tests/testthat/test-foofy.R}, respectively. +in \code{tests/testthat/test-foofy.R} and the results of any snapshot tests live +in \code{tests/testthat/snaps/foofy.md}. + +You can use them to create new files from scratch by supplying \code{name}, or +if you use RStudio or Positron, you can call to create (or navigate to) the +companion file based on the currently open file. } \section{Renaming files in an existing package}{ diff --git a/man/use_rcpp.Rd b/man/use_rcpp.Rd index 2cd79b614..2a482b123 100644 --- a/man/use_rcpp.Rd +++ b/man/use_rcpp.Rd @@ -17,7 +17,7 @@ use_c(name = NULL) } \arguments{ \item{name}{Either a string giving a file name (without directory) or -\code{NULL} to take the name from the currently open file in RStudio.} +\code{NULL} to take the name from the currently open file in RStudio/Positron.} } \description{ Adds infrastructure commonly needed when using compiled code: diff --git a/man/use_snapshot.Rd b/man/use_snapshot.Rd deleted file mode 100644 index ee018be9c..000000000 --- a/man/use_snapshot.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/r.R -\name{use_snapshot} -\alias{use_snapshot} -\title{Edit the snapshot file associated with an R or test file} -\usage{ -use_snapshot(name = NULL, open = rlang::is_interactive()) -} -\arguments{ -\item{name}{Either a string giving a file name (without directory) or -\code{NULL} to take the name from the currently open file in RStudio.} - -\item{open}{Whether to open the file for interactive editing.} -} -\description{ -This function opens the snapshot file associated with an R or test file, if -one exists. -} From 773a6e137af45fd6803fda1297cdaf9f0a5f4ca9 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Mon, 2 Feb 2026 16:10:53 -0600 Subject: [PATCH 8/8] Upate news bullet --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 017ff696a..a6a597210 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,7 +3,7 @@ * Removes deprecated `use_tidy_style()` from to-do's from upkeep (@edgararuiz) * `pr_resume()` (without a specific `branch`) and `pr_fetch()` (without a specific `number`) no longer error when a branch name contains curly braces (#2107, @jonthegeek). -* `use_snapshot()` is a new function to open a [testthat snapshot file]() corresponding to a given test file or R file (#2156, @jonthegeek). +* `use_snapshot()` is a new function to open a testthat snapshot file corresponding to a given test/R file (#2156, @jonthegeek). # usethis 3.2.1