diff --git a/DESCRIPTION b/DESCRIPTION index dc4ddc87..0f9fbdd6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,6 +22,7 @@ Imports: jsonlite, openssl, rlang, + rstudioapi, withr, yaml Suggests: diff --git a/R/interactive.R b/R/interactive.R index e97200f6..1d078cfb 100644 --- a/R/interactive.R +++ b/R/interactive.R @@ -1,16 +1,54 @@ +is_plausible_orderly_report <- function(path) { + path_split <- fs::path_split(path)[[1]] + + length(path_split) > 2 && + path_split[[length(path_split) - 1]] == "src" && + file.exists(file.path(path, "../..", "orderly_config.yml")) +} + +rstudio_get_current_active_editor_path <- function() { + if (rstudioapi::isAvailable()) { + rstudioapi::getSourceEditorContext()$path + } else { + NULL + } +} + ## This is something that we might improve over time - it will likely ## be useful to have some sort of "register interactive" function ## which we could then just look up. ## ## I am not sure if we also want to allow working interactively from a ## draft directory too. -detect_orderly_interactive_path <- function(path = getwd()) { - path_split <- fs::path_split(path)[[1]] - is_plausible <- length(path_split) > 2 && - path_split[[length(path_split) - 1]] == "src" && - file.exists(file.path(path, "../..", "orderly_config.yml")) - if (!is_plausible) { - stop(sprintf("Failed to detect orderly path at '%s'", path)) +detect_orderly_interactive_path <- function(path = getwd(), + editor_path = rstudio_get_current_active_editor_path()) { + is_valid <- is_plausible_orderly_report(path) + suggested_wd <- NULL + if (!is.null(editor_path)) { + dir <- fs::path_dir(editor_path) + if (paths_are_different(dir, path) && is_plausible_orderly_report(dir)) { + suggested_wd <- dir + } + } + + if (!is_plausible_orderly_report(path)) { + msg <- c("Working directory {.path {path}} is not a valid orderly report.") + if (!is.null(suggested_wd)) { + cli::cli_abort( + c(msg, i = "Use {.code setwd({.str {suggested_wd}})} to set the working directory to the report currently open in RStudio.")) + } else { + cli::cli_abort(msg) + } + } + + if (!is.null(suggested_wd)) { + # For some reason, cli_warn has a different behaviour than cli_abort and + # doesn't make individual bullet points available in the condition object. + # The following mimicks cli_abort more closely, making testing easier. + msg <- c( + "Working directory {.path {path}} does not match the report currently open in RStudio.", + i = "Use {.code setwd({.str {suggested_wd}})} to switch working directories.") + rlang::warn(vcapply(msg, cli::format_inline), use_cli_format = TRUE) } as.character(fs::path_norm(file.path(path, "../.."))) } diff --git a/R/util.R b/R/util.R index 0c9bdcdc..5faa86c2 100644 --- a/R/util.R +++ b/R/util.R @@ -632,3 +632,12 @@ saverds_atomic <- function(data, path, allow_fail = FALSE) { finally = unlink(tmp)) } } + + +paths_are_different <- function(x, y) { + tryCatch({ + x_real <- fs::path_real(x) + y_real <- fs::path_real(y) + x_real != y_real + }, error = function(e) FALSE) +} diff --git a/tests/testthat/helper-orderly.R b/tests/testthat/helper-orderly.R index 9ce5646d..48cd9f84 100644 --- a/tests/testthat/helper-orderly.R +++ b/tests/testthat/helper-orderly.R @@ -1,6 +1,6 @@ options(outpack.schema_validate = requireNamespace("jsonvalidate", quietly = TRUE) && - packageVersion("jsonvalidate") >= "1.4.0", + utils::packageVersion("jsonvalidate") >= "1.4.0", orderly_index_progress = FALSE) diff --git a/tests/testthat/test-interactive.R b/tests/testthat/test-interactive.R index 1fb86052..1d46063d 100644 --- a/tests/testthat/test-interactive.R +++ b/tests/testthat/test-interactive.R @@ -1,18 +1,58 @@ test_that("can detect orderly directory", { - path <- test_prepare_orderly_example("explicit") - envir <- new.env() - id <- orderly_run_quietly("explicit", root = path, envir = envir) + root <- test_prepare_orderly_example("explicit") + detected_root <- detect_orderly_interactive_path(file.path(root, "src", "explicit")) + expect_equal(detected_root) +}) + +test_that("errors when working directory is not report", { + root <- test_prepare_orderly_example("explicit") expect_error( - detect_orderly_interactive_path(path), - "Failed to detect orderly path at") + detect_orderly_interactive_path(root), + "Working directory .* is not a valid orderly report.") + expect_error( - detect_orderly_interactive_path(file.path(path, "src")), - "Failed to detect orderly path at") - root <- detect_orderly_interactive_path(file.path(path, "src", "explicit")) - expect_equal(path, root) + detect_orderly_interactive_path(file.path(root, "src")), + "Working directory .* is not a valid orderly report.") +}) + +test_that("suggests changing working directory", { + # Make matching simpler by avoiding line-wrapping. + withr::local_options(cli.width=Inf) + + root <- test_prepare_orderly_example(c("explicit", "implicit")) + + e <- expect_error(detect_orderly_interactive_path( + path = file.path(root, "src"), + editor_path = file.path(root, "src", "implicit", "orderly.R")), + "Working directory .* is not a valid orderly report") + expect_match(e$body[[1]], "Use `setwd(.*)` to set the working directory to the report currently open in RStudio") + + w <- expect_warning(detect_orderly_interactive_path( + path = file.path(root, "src", "explicit"), + editor_path = file.path(root, "src", "implicit", "orderly.R")), + "Working directory .* does not match the report currently open in RStudio") + expect_match(w$body[[1]], "Use `setwd(.*)` to switch working directories") }) +test_that("does not unnecessarily suggest changing working directory", { + root <- test_prepare_orderly_example("explicit") + + expect_no_warning(detect_orderly_interactive_path( + path = file.path(root, "src", "explicit"), + editor_path = "Untitled" + )) + + expect_no_warning(detect_orderly_interactive_path( + path = file.path(root, "src", "explicit"), + editor_path = file.path(root, "src", "explicit", "orderly.R") + )) + + expect_no_warning(detect_orderly_interactive_path( + path = file.path(root, "src", "explicit"), + editor_path = file.path(root, "orderly_config.yml") + )) +}) test_that("can validate interactive parameters", { mock_readline <- mockery::mock("TRUE", "100", "1.23", '"string"')