diff --git a/DESCRIPTION b/DESCRIPTION index 0911389..592f447 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -42,4 +42,4 @@ Suggests: Config/testthat/edition: 3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.1.9000 diff --git a/NAMESPACE b/NAMESPACE index 175ae06..1455f4e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,7 @@ S3method(print,outline_report) export(active_rs_doc) export(active_rs_doc_copy) export(active_rs_doc_delete) +export(active_rs_doc_nav) export(arrange_identity) export(browse_pkg) export(case_if_any) diff --git a/NEWS.md b/NEWS.md index 830a931..8b5b42d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,16 @@ * `proj_list()` / `proj_switch()` no longer opens a nested project if looking for `"pkgdown"`, `"testthat"`, etc. +* `active_rs_doc_nav()` is a new function to navigate to files pane location. + +`active_rs_doc_copy()` now accepts copying md and qmd files too and no longer allows renaming Rprofile. + +* `proj_file()` is better. + +* Local GitHub issues show better in outline. + +* `file_outline()` detects better plot titles and section titles. + # reuseme 0.0.2 * `complete_todo()` no longer deletes the full line. It only deletes what it says it deletes (#27). diff --git a/R/browse-pkg.R b/R/browse-pkg.R index da80eb0..bfcbf00 100644 --- a/R/browse-pkg.R +++ b/R/browse-pkg.R @@ -88,9 +88,9 @@ browse_pkg <- function(package = NULL, "reference" ) - pkgdown <- stringr::str_remove(pkgdown, "/$") + pkgdown <- sub("/$", "", pkgdown) pkgdown_tabs_url <- paste0(pkgdown, "/", pkgdown_tabs, "/") - if (stringr::str_detect(pkgdown, "r-lib.org|tidyverse.org|tidymodels.org") && !stringr::str_detect(pkgdown, "github.com")) { + if (grepl("r-lib.org|tidyverse.org|tidymodels.org", pkgdown) && !grepl("github.com", pkgdown, fixed = TRUE)) { # known packages with dev enabled. pkgdown_tabs_url[1] <- paste0(pkgdown, "/dev/news") } diff --git a/R/dplyr-plus.R b/R/dplyr-plus.R index 7b68c75..6815951 100644 --- a/R/dplyr-plus.R +++ b/R/dplyr-plus.R @@ -240,10 +240,10 @@ filter_if_any <- function(.data, ..., .by = NULL, .keep_new_var = FALSE) { if (all(purrr::map_lgl(variables[, 1:n_var], is.logical))) { res <- dplyr::filter(variables, dplyr::if_any(.cols = seq_len(n_var)), .by = {{ .by }}) - if (!.keep_new_var) { - res <- res[-seq_len(n_var)] - } else { + if (.keep_new_var) { cli::cli_warn("You have modified the original data") + } else { + res <- res[-seq_len(n_var)] } return(res) @@ -310,14 +310,13 @@ extract_cell_value <- function(data, var, filter, name = NULL, length = NULL, un if (unique) { res2 <- unique_named(res2) } - if (!is.null(length)) { + + if (!is.null(length) && !rlang::has_length(res2, length)) { # TODO use `check_length()` when implemented. r-lib/rlang#1618 - if (!rlang::has_length(res2, length)) { - cli::cli_abort(c( - "Expected an output of {length}", - "Got an output of {length(res2)}" - )) - } + cli::cli_abort(c( + "Expected an output of {length}", + "Got an output of {length(res2)}" + )) } res2 diff --git a/R/escape-inline-markup.R b/R/escape-inline-markup.R index f33e140..372e1e2 100644 --- a/R/escape-inline-markup.R +++ b/R/escape-inline-markup.R @@ -28,12 +28,12 @@ escape_markup <- function(x) { is_markup_okay <- is_bracket & stringr::str_detect(x, "\\{\\.[:alpha:]+[^\\{]") & !local_var_ref_in_markup & !is_markup_incorrect(x) if (all(is_markup_okay) && !any(local_var_ref_in_markup)) { - x[is_left_bracket & !is_bracket] <- stringr::str_replace_all(x[is_left_bracket & !is_bracket], "\\{", "{{") - x[is_right_bracket & !is_bracket] <- stringr::str_replace_all(x[is_right_bracket & !is_bracket], "\\}", "}}") + x[is_left_bracket & !is_bracket] <- gsub("{", "{{", x[is_left_bracket & !is_bracket], fixed = TRUE) + x[is_right_bracket & !is_bracket] <- gsub("}", "}}", x[is_right_bracket & !is_bracket], fixed = TRUE) return(x) } # replace fn arg {fn}(arg) -> fn({arg}) - valid_r_variable_regex <- "[:alpha:][[:alpha:]\\_\\d]+" + valid_r_variable_regex <- "\\.?[:alpha:][[:alpha:]\\_\\d]+" x <- stringr::str_replace_all( x, paste0("\\{(", valid_r_variable_regex, ")\\}\\("), @@ -59,16 +59,20 @@ escape_markup <- function(x) { # ) # replace variables + x <- replace_r_var(x) + x[is_left_bracket & !is_bracket] <- gsub("{", "{{", x[is_left_bracket & !is_bracket], fixed = TRUE) + x[is_right_bracket & !is_bracket] <- gsub("}", "}}", x[is_right_bracket & !is_bracket], fixed = TRUE) - x <- replace_r_var(x) + # whisker replacement {{{ vignette_title }}} in usethis by vignette_title + x <- stringr::str_replace_all(x, "\\{\\{?\\{?(\\s?[^\\}]+) \\}?\\}?\\}", "\\1") - x[is_left_bracket & !is_bracket] <- stringr::str_replace_all(x[is_left_bracket & !is_bracket], "\\{", "{{") - x[is_right_bracket & !is_bracket] <- stringr::str_replace_all(x[is_right_bracket & !is_bracket], "\\}", "}}") + # make `{` and `}` work + x <- stringr::str_replace_all(x, c("`\\{`" = "`{{`", "`\\}`" = "`}}`")) if (any(stringr::str_detect(x, "\\{{3,10}"))) { # more than 3 { - cli::cli_abort("internal errror. Did not transform string correctly.") + rlang::abort(c("internal error. Did not transform string correctly.", x)) } x } @@ -84,7 +88,7 @@ escape_markup <- function(x) { #' is_markup_incorrect("{.file {gt}}") is_markup_incorrect <- function(x) { # no match of single { or } - valid_r_variable_regex <- "[:alpha:][[:alpha:]\\_\\d]+" + valid_r_variable_regex <- "\\.?[:alpha:][[:alpha:]\\_\\d]+" stringr::str_detect(x, pattern = paste0("(? "{{gt_var}} in {{gt_var}} in gt_var in {.file {gt_var}}." +#' # last instance taken care of with escape_markup with a different strategy #' escape_markup("{gt_var} in {{gt_var}} in gt_var in {.file {gt_var}}.") #' #> "{{gt_var}} in {{gt_var}} in gt_var in {.file gt_var}." NULL diff --git a/R/files-conflicts.R b/R/files-conflicts.R index 511a1d9..c3175d4 100644 --- a/R/files-conflicts.R +++ b/R/files-conflicts.R @@ -118,7 +118,7 @@ solve_file_name_conflict <- function(files, regex, dir = ".", extra_msg = NULL, cli::cli_inform } # Remove duplicated Found x references - which_bullet_to_replace <- stringr::str_subset(extra_msg, "Found references to", negate = TRUE) + which_bullet_to_replace <- stringr::str_subset(extra_msg, stringr::fixed("Found references to"), negate = TRUE) # possibly just move up our # extra_msg[i] <- f_inform(c( @@ -152,8 +152,8 @@ get_referenced_files <- function(files) { stringr::str_subset(pattern = "file.[(exist)|(delete)]|glue\\:\\:glue|unlink", negate = TRUE) |> # don't detect where we test for existence of path or construct a path with glue stringr::str_subset(pattern = "[(regexp)|(pattern)]\\s\\=.*\".*[:alpha:]\"", negate = TRUE) |> # remove regexp = a.pdf format stringr::str_subset(pattern = "grepl?\\(|stringr|g?sub\\(", negate = TRUE) |> # avoid regexp - stringr::str_subset(pattern = "nocheck", negate = TRUE) |> # remove nocheck and unlink statements (refers to deleted files anywa) - stringr::str_subset("\"") |> + stringr::str_subset(pattern = stringr::fixed("nocheck"), negate = TRUE) |> # remove nocheck and unlink statements (refers to deleted files anywa) + stringr::str_subset(stringr::fixed("\"")) |> stringr::str_trim() |> stringr::str_extract_all("\"[^\"]+\"") |> unlist() |> @@ -163,7 +163,7 @@ get_referenced_files <- function(files) { stringr::str_subset(pattern = "tmp|temp", negate = TRUE) |> # remove common file names that are not very nice stringr::str_subset(pattern = "https?", negate = TRUE) |> # doesn't check for files read online. stringr::str_subset(pattern = "\\@.+\\.", negate = TRUE) |> # email addresses or containing @ - stringr::str_subset(pattern = "_fichiers/", negate = TRUE) |> # manually remove false positive + stringr::str_subset(pattern = stringr::fixed("_fichiers/"), negate = TRUE) |> # manually remove false positive stringr::str_subset(pattern = "\n", negate = TRUE) |> # remove things with line breaks stringr::str_subset(pattern = "^\\.[:alpha:]{1,4}$", negate = TRUE) |> # remove reference to only file extensions stringr::str_subset(pattern = "\\.\\d+$", negate = TRUE) |> # remove 0.000 type diff --git a/R/markup.R b/R/markup.R index 1a4f90d..464cc7a 100644 --- a/R/markup.R +++ b/R/markup.R @@ -9,14 +9,14 @@ #' #' Afterwards, we use [markup_href()] to create a cli link #' @param x A string, usually lines of files that contains issue numbers. -#' +#' @param home_repo Optional, but if supplied, will be stripped. #' @return A markdown link linked issue to GitHub issue #' @export #' @keywords internal #' @family inline markup internal helpers #' @examples #' link_gh_issue(c("We really need rstudio/gt#1469 to be fixed.")) -link_gh_issue <- function(x) { +link_gh_issue <- function(x, home_repo = NULL) { # Return early if no issue pattern is detected. regex_gh_issue <- common_regex("gh_issue") @@ -37,11 +37,73 @@ link_gh_issue <- function(x) { regex_gh_issue, paste0("[\\1#\\2](https://github.com/\\1/issues/\\2)") ) + if (!is.null(home_repo)) { + x_changed <- gsub( + paste0(home_repo, "#"), + "#", + x_changed + ) + } x[has_gh_issue] <- x_changed x } +# transforms (#xx) to (org/repo#xx) +link_local_gh_issue <- function(x, repo_home) { + gsub( + # max 99999 issues. + pattern = "\\((#\\d{1,5})\\)", + paste0("(", repo_home, "\\1)"), + x + ) +} +find_pkg_org_repo <- function(dir_common = NULL, file = NULL) { + rlang::local_interactive(FALSE) + withr::local_options("usethis.quiet" = TRUE) + if (!is.null(dir_common)) { + pkg_path <- tryCatch( + rprojroot::find_package_root_file(path = dir_common), + error = function(e) { + # cli::cli_inform("Could not detect path.") + NULL + } + ) + if (is.null(pkg_path)) { + return(NULL) + } + gh_url <- tryCatch( + usethis::browse_github(basename(pkg_path)), + error = function(e) { + # TODO possibly look into checking desc::desc_get("BugReports", "~/path/to/DESCRIPTION") + # cli::cli_abort("didn't find a way to do what is required.", parent = e) + NULL + } + ) + if (is.null(gh_url)) { + return(NULL) + } + org_repo_found <- sub(".+github.com/|.+gitlab.com/", "", gh_url) + return(org_repo_found) + } + + if (!is.null(file)) { + pkg_path <- withCallingHandlers( + rprojroot::find_package_root_file(path = file), + error = function(e) { + # cli::cli_inform("Could not detect path.") + NULL + } + ) + gh_url <- usethis::browse_github(basename(pkg_path)) + org_repo_found <- sub(".+github.com/|.+gitlab.com/", "", gh_url) + } else { + org_repo_found <- NULL + } + if (is.null(org_repo) && is.null(org_repo_found)) { + cli::cli_abort("No way to discover URL.") + } +} #' Create a cli href with a markdown link #' #' Transforms `[text](url)` -> `{.href [text](url)}` diff --git a/R/open.R b/R/open.R index 67d3b42..71f098d 100644 --- a/R/open.R +++ b/R/open.R @@ -39,10 +39,10 @@ open_rs_doc <- function(path, line = -1L, col = -1L, move_cursor = TRUE) { #' @name open_rs_doc #' @export active_rs_doc <- function() { - if (!interactive() && !rstudioapi::isAvailable()) { + if (!interactive() && !is_rstudio()) { return("Non-existing doc") } - if (!rstudioapi::isAvailable()) { + if (!is_rstudio()) { cli::cli_abort("Not in RStudio.") } unsaved_doc <- tryCatch(rstudioapi::documentPath(), error = function(e) TRUE) @@ -79,20 +79,21 @@ active_rs_doc_copy <- function(new = NULL, ..., old = NULL) { cli::cli_abort("Unsaved document, focus on the saved doc you want to save.") } - if (!fs::path_ext(old) %in% c("R", "qmd", "Rmd")) { - cli::cli_abort("Only R docs for now") + if (!fs::path_ext(old) %in% c("md", "R", "qmd", "Rmd")) { + cli::cli_abort("Only R and md docs for now") } old_path_file <- fs::path_ext_remove(fs::path_file(old)) - if (stringr::str_detect(old, "r-profile|Rprofile")) { + + if (grepl("r-profile|Rprofile", old)) { cli::cli_abort("Attempting to copy Rprofile (focus on the document you want)") } if (is.null(new)) { new_name <- paste0(old_path_file, "-new") } else { - new_name <- stringr::str_remove(new, "\\.R|\\.qmd|\\.Rmd$") + new_name <- sub("\\.R|\\.[Rq]?md$", "", new) } # Hack to ensure file/file.R will be correctly renamed. - new_path <- stringr::str_replace(old, paste0(old_path_file, "\\."), paste0(new_name, ".")) + new_path <- sub(paste0(old_path_file, "\\."), paste0(new_name, "."), old) copied <- file.copy(old, new_path, overwrite = FALSE) if (copied) { @@ -124,7 +125,7 @@ active_rs_doc_copy <- function(new = NULL, ..., old = NULL) { #' @examplesIf FALSE #' active_rs_doc_delete() active_rs_doc_delete <- function() { - if (!rlang::is_interactive() || !rstudioapi::isAvailable()) { + if (!rlang::is_interactive() || !is_rstudio()) { cli::cli_abort(c("Can't delete files in non-interactive sessions.")) } doc <- active_rs_doc() @@ -140,7 +141,7 @@ active_rs_doc_delete <- function() { if (fs::is_dir(elems$full_path)) { cli::cli_abort("Must be a file", .internal = TRUE) } - if (interactive() && rstudioapi::isAvailable()) { + if (interactive() && is_rstudio()) { rstudioapi::documentSave() } cli::cli_inform(c( @@ -211,7 +212,7 @@ active_rs_doc_delete <- function() { parent_dir <- fs::path_file(fs::path_dir(elems$full_path)) if (grepl("^temp", fs::path_file(elems$rel_path)) || - (!parent_dir %in% c("tests", "testthat") && grepl("^test-", fs::path_file(elems$rel_path)))) { + (!parent_dir %in% c("tests", "testthat") && grepl("^test-", fs::path_file(elems$rel_path)))) { reasons_deleting <- c(reasons_deleting, "it has the temp- prefix.") will_delete <- append(will_delete, TRUE) } @@ -256,7 +257,7 @@ active_rs_doc_delete <- function() { cli::cli_inform(c( "v" = "Deleted the active document {.val {elems$rel_path}} because {reasons_deleting}.", # FIXME (upstream) the color div doesn't go all the way r-lib/cli#694 - "i" = paste(cli::col_grey("The deleted file"), "{.path {elems$full_path}}", cli::col_grey("contents are returned invisibly in case you need them.")) + "i" = paste(cli::col_grey("The deleted file"), "{.path {elems$full_path}}", cli::col_grey("contents are returned invisibly in case you need them.")) )) contents <- readLines(elems$full_path, encoding = "UTF-8") fs::file_delete(elems$full_path) @@ -361,3 +362,36 @@ normalize_proj_and_path <- function(path, call = caller_env()) { full_path = full_path ) } + +#' Open Files Pane at current document location +#' +#' Easily navigate to active file document. +#' +#' Wrapper around [executeCommand("activateFiles")][rstudioapi::executeCommand()] + +#' [rstudioapi::filesPaneNavigate()] + [rstudioapi::getActiveDocumentContext()] +#' +#' @param path A path to file to navigate to (default active document). +#' +#' @returns NULL, called for its side effects. +#' @export +active_rs_doc_nav <- function(path = active_rs_doc()) { + if (!is_rstudio() || !interactive()) { + cli::cli_abort("Must use in RStudio interactive sessions.") + } + if (is.null(path)) { + cli::cli_abort("Can't navigate to an unsaved file!") + } + if (fs::is_file(path)) { + dir <- fs::path_dir(path) + } else if (fs::is_dir(path)) { + dir <- path + } else { + cli::cli_abort("{.arg path} must be an existing file or directory.") + } + rstudioapi::executeCommand("activateFiles") + rstudioapi::filesPaneNavigate(dir) + cli::cli_inform(c( + "v" = "Navigated to {.path {dir}} in RStudio Files Pane." + )) + invisible() +} diff --git a/R/outline-criteria.R b/R/outline-criteria.R index f7322ab..961cc60 100644 --- a/R/outline-criteria.R +++ b/R/outline-criteria.R @@ -33,7 +33,7 @@ o_is_roxygen_comment <- function(x, file_ext = NULL) { } o_is_todo_fixme <- function(x) { - has_todo <- stringr::str_detect(x, "(?|\\(\\.*\\)"), + is_section_title_source = is_section_title & + stringr::str_detect(content, "[-\\=]{3,}|^\\#'") & + stringr::str_detect(content, "[:alpha:]"), is_todo_fixme = print_todo & o_is_todo_fixme(content) & !o_is_roxygen_comment(content, file_ext) & !is_snap_file, n_leading_hash = nchar(stringr::str_extract(content, "\\#+")), n_leading_hash = dplyr::coalesce(n_leading_hash, 0), + # Make sure everything is second level in revdep/. + n_leading_hash = n_leading_hash + grepl("revdep/", file, fixed = TRUE), is_second_level_heading_or_more = (is_section_title_source | is_section_title) & n_leading_hash > 1, is_cross_ref = stringr::str_detect(content, "docs_links?\\(") & !stringr::str_detect(content, "@param|\\{\\."), - is_function_def = grepl("<- function(", content, fixed = TRUE) & !stringr::str_starts(content, "\\s*#") + is_function_def = grepl("<- function(", content, fixed = TRUE) & !stringr::str_starts(content, "\\s*#"), + is_tab_or_plot_title = o_is_tab_plot_title(content) & !is_section_title & !is_function_def, + is_a_comment_or_code = stringr::str_detect(content, "!=|\\|\\>|\\(\\.*\\)"), ) x <- dplyr::mutate( x, - before_and_after_empty = line == 1 | !nzchar(dplyr::lead(content, default = "")) & !nzchar(dplyr::lag(content)), - .by = file + before_and_after_empty = + line == 1 | !nzchar(dplyr::lead(content, default = "")) & !nzchar(dplyr::lag(content)), + .by = "file" ) x } diff --git a/R/outline.R b/R/outline.R index 599c6af..fc652c5 100644 --- a/R/outline.R +++ b/R/outline.R @@ -90,7 +90,7 @@ file_outline <- function(pattern = NULL, recent_only = FALSE) { # To contribute to this function, take a look at .github/CONTRIBUTING - if (length(path) == 1L && interactive() && rstudioapi::isAvailable()) { + if (length(path) == 1L && rlang::is_interactive() && is_rstudio()) { is_active_doc <- identical(path, active_rs_doc()) } else { is_active_doc <- FALSE @@ -134,8 +134,8 @@ file_outline <- function(pattern = NULL, file_content <- dplyr::bind_rows(file_content, .id = "file") } - suppressMessages( - in_active_project <- tryCatch( + in_active_project <- suppressMessages( + tryCatch( identical(suppressWarnings(proj_get2()), dir_common), error = function(e) FALSE ) @@ -197,7 +197,7 @@ file_outline <- function(pattern = NULL, } # File outline =================== # strip outline element .data$outline = `# Section 1` becomes `Section 1` - file_sections1 <- display_outline_element(file_sections0) + file_sections1 <- display_outline_element(file_sections0, dir_common) # Create hyperlink in console file_sections <- construct_outline_link(file_sections1, is_saved_doc, dir_common, pattern) @@ -215,7 +215,7 @@ file_outline <- function(pattern = NULL, ) file_sections$recent_only <- recent_only - if (any(duplicated(file_sections$outline_el))) { + if (anyDuplicated(file_sections$outline_el) > 0L) { file_sections <- scrub_duplicate_outline(file_sections) } file_sections <- dplyr::relocate( @@ -251,14 +251,14 @@ proj_outline <- function(pattern = NULL, proj = proj_get2(), work_only = TRUE, d )) } - if (!fs::dir_exists(proj)) { # when referring to a project by name. - proj_dir <- proj_list(proj) - } else { + if (fs::dir_exists(proj)) { if (!is_active_proj) { cli::cli_warn("Use {.fn dir_outline} for that.") } proj_dir <- proj + } else { # when referring to a project by name. + proj_dir <- proj_list(proj) } if (!rlang::has_length(proj_dir, 1)) { @@ -310,11 +310,7 @@ dir_outline <- function(pattern = NULL, path = ".", work_only = TRUE, dir_tree = if (recurse && !identical(Sys.getenv("TESTTHAT"), "true")) { # Remove examples from outline and test example files to avoid clutter # examples don't help understand a project. - file_list_to_outline <- fs::path_filter( - file_list_to_outline, - regexp = "testthat/_ref/|example-file", - invert = TRUE - ) + file_list_to_outline <- exclude_example_files(file_list_to_outline) } if (any(grepl("README.Rmd", file_list_to_outline))) { @@ -325,7 +321,7 @@ dir_outline <- function(pattern = NULL, path = ".", work_only = TRUE, dir_tree = fs::dir_tree( path = dir, - regexp = "R/.+|qmd|Rmd|_files|~\\$|*.Rd|_snaps|testthat.R|Rmarkdown|docs/", + regexp = "R/.+|qmd|Rmd|_files|~\\$|*.Rd|_snaps|tests/testthat.R|Rmarkdown|docs/", recurse = recurse, invert = TRUE ) @@ -333,6 +329,28 @@ dir_outline <- function(pattern = NULL, path = ".", work_only = TRUE, dir_tree = file_outline(path = file_list_to_outline, pattern = pattern, work_only = work_only, dir_common = dir, alpha = alpha, recent_only = recent_only) } +exclude_example_files <- function(path) { + # styler tests examples may not work.. + + regexp_exclude <- paste( + "vignettes/test/", # test vignettes + "LICENSE.md", # avoid indexing this. + "tests/(performance-monitor|gt-examples/|testthat/scope-|testthat/assets|testthat/_outline|testthat/testTestWithFailure|testthat/testTest/|testthat/test-parallel/|testthat/test-list-reporter/)", # example files in usethis, pkgdown, reuseme, devtools, etc. + "inst/((rmarkdown/)?templates/|example-file/|examples/rmd/|tutorials/)", # license templates in usethis + "revdep/", # likely don't need to outline revdep/, use dir_outline() to find something in revdep/ + "themes/hugo-theme-console/", # protect blogdown + "vignettes/.+\\.R$", # generated files + "RcppExports.R", + "pkgdown/assets", + sep = "|" + ) + + fs::path_filter( + path, + regexp = regexp_exclude, + invert = TRUE + ) +} # Print method ------------------- #' @export @@ -419,7 +437,16 @@ print.outline_report <- function(x, ...) { base_name <- c(base_name, " ", title_el) } - cli::cli_h3(base_name) + # TRICK need tryCatch when doing something, withCallingHandlers when only rethrowing? + tryCatch( + cli::cli_h3(base_name), + error = function(e) { + # browser() + cli::cli_h3(escape_markup(base_name)) + # print(base_name) + # rlang::abort("Could not parse by cli", parent = e) + } + ) if (recent_only) { if (i %in% is_recently_modified) { @@ -465,7 +492,7 @@ keep_outline_element <- function(.data) { # What to keep in .R files (!is_md & is_section_title_source) | # What to keep anywhere - is_tab_or_plot_title | is_todo_fixme | is_test_name | is_cross_ref | is_function_def # | is_cli_info # TODO reanable cli info + is_tab_or_plot_title | is_todo_fixme | is_test_name | is_cross_ref | is_function_def # | is_cli_info # TODO reanable cli info ) dat$simplify_news <- NULL dat @@ -475,23 +502,28 @@ keep_outline_element <- function(.data) { # Includes removing headings comments # Remove title = # Removing quotes, etc. -display_outline_element <- function(.data) { +display_outline_element <- function(.data, dir_common) { x <- .data - x$outline_el <- purrr::map_chr(x$content, link_gh_issue) # to add link to GitHub. + org_repo <- find_pkg_org_repo(dir_common, unique(x$file)) + if (!is.null(org_repo)) { + x$outline_el <- link_local_gh_issue(x$content, org_repo) + } else { + x$outline_el <- x$content + } + x$outline_el <- purrr::map_chr(x$outline_el, \(x) link_gh_issue(x, org_repo)) # to add link to GitHub. x$outline_el <- purrr::map_chr(x$outline_el, markup_href) x <- dplyr::mutate( x, outline_el = dplyr::case_when( - is_todo_fixme ~ stringr::str_extract(outline_el, "(TODO.+)|(FIXME.+)|(WORK.+)"), - is_test_name ~ stringr::str_extract(outline_el, "test_that\\(['\"](.+)['\"]", group = 1), + is_todo_fixme ~ stringr::str_extract(outline_el, "(TODO.+)|(FIXME.+)|(WORK.+)|(BOOK.+)"), + is_test_name ~ stringr::str_extract(outline_el, "test_that\\(['\"](.+)['\"],\\s?\\{", group = 1), is_cli_info ~ stringr::str_extract(outline_el, "[\"'](.{5,})[\"']") |> stringr::str_remove_all("\""), - is_tab_or_plot_title ~ stringr::str_extract(outline_el, "title = [\"']([^\"]{5,})[\"']", group = 1), - is_chunk_cap_next & !is_chunk_cap ~ stringr::str_remove_all(outline_el, "\\s?\\#\\|\\s+"), + is_tab_or_plot_title ~ stringr::str_extract(outline_el, "title =[^\"']*[\"']([^\"]{5,})[\"']", group = 1), is_chunk_cap_next & !is_chunk_cap ~ stringr::str_remove_all(outline_el, "\\s?\\#\\|\\s+"), is_chunk_cap ~ stringr::str_remove_all(stringr::str_extract(outline_el, "(cap|title)\\:\\s*(.+)", group = 2), "\"|'"), is_cross_ref ~ stringr::str_remove_all(outline_el, "^(i.stat\\:\\:)?.cdocs_lin.s\\(|[\"']\\)$|\""), is_doc_title ~ stringr::str_remove_all(outline_el, "subtitle\\:\\s?|title\\:\\s?|\"|\\#\\|\\s?"), is_section_title & !is_md ~ stringr::str_remove(outline_el, "^\\s{0,4}\\#+\\s+|^\\#'\\s\\#+\\s+"), # Keep inline markup - is_section_title & is_md ~ stringr::str_remove_all(outline_el, "^\\#+\\s+|\\{.+\\}"), # strip cross-refs. + is_section_title & is_md ~ stringr::str_remove_all(outline_el, "^\\#+\\s+|\\{.+\\}|<(a href|img src).+$"), # strip cross-refs. is_function_def ~ stringr::str_extract(outline_el, "(.+)\\<-", group = 1) |> stringr::str_trim(), .default = stringr::str_remove_all(outline_el, "^\\s*\\#+\\|?\\s?(label:\\s)?|\\s?[-\\=]{4,}") ), @@ -570,13 +602,13 @@ define_important_element <- function(.data) { } construct_outline_link <- function(.data, is_saved_doc, dir_common, pattern) { - rs_avail_file_link <- rstudioapi::isAvailable("2023.09.0.375") # better handling after + rs_avail_file_link <- is_rstudio("2023.09.0.375") # better handling after .data <- define_important_element(.data) if (is.null(dir_common) || !nzchar(dir_common)) { dir_common <- "Don't remove anything if not null" } - .data$rs_version <- ifelse(!rstudioapi::isAvailable("2023.12.0.274") && rstudioapi::isAvailable(), ".", "") + .data$rs_version <- ifelse(!is_rstudio("2023.12.0.274") && is_rstudio(), ".", "") .data$has_inline_markup <- dplyr::coalesce(stringr::str_detect(.data$outline_el, "\\{|\\}"), FALSE) .data$is_saved_doc <- is_saved_doc .data <- dplyr::mutate( @@ -625,20 +657,17 @@ construct_outline_link <- function(.data, is_saved_doc, dir_common, pattern) { outline_el2 = dplyr::coalesce(outline_el2, outline_el) ) - .data <- dplyr::mutate(.data, - link = paste0(outline_el2, " {.path ", file, ":", line, "}"), - # rstudioapi::documentOpen works in the visual mode!! but not fully. - file_path = .data$file, - is_saved_doc = .env$is_saved_doc, - - # May have caused CI failure - text_in_link = stringr::str_remove(file_path, as.character(.env$dir_common)) |> stringr::str_remove("^/"), - # decide which is important - style_fun = dplyr::case_match(importance, - "not_important" ~ "cli::style_italic('i')", # cli::style_inverse for bullets - "important" ~ "cli::style_inverse('i')", - .default = NA - ) + .data$link <- paste0(.data$outline_el2, " {.path ", .data$file, ":", .data$line, "}") + # rstudioapi::documentOpen works in the visual mode!! but not fully. + .data$file_path <- .data$file + .data$is_saved_doc <- is_saved_doc + # May have caused CI failure + .data$text_in_link <- sub(as.character(dir_common), "", .data$file_path) + .data$text_in_link <- sub("^/", "", .data$text_in_link) + .data$style_fun <- dplyr::case_match(.data$importance, + "not_important" ~ "cli::style_italic('i')", # cli::style_inverse for bullets + "important" ~ "cli::style_inverse('i')", + .default = NA_character_ ) if (anyNA(.data$style_fun)) { @@ -664,7 +693,17 @@ construct_outline_link <- function(.data, is_saved_doc, dir_common, pattern) { rs_version = NULL, outline_el2 = NULL, condition_to_truncate = NULL, - condition_to_truncate2 = NULL + condition_to_truncate2 = NULL, + style_fun = NULL, + is_saved_doc = NULL, + is_roxygen_comment = NULL, + is_news = NULL, + # I may put it pack + importance = NULL, + # may be useful for debugging + before_and_after_empty = NULL, + # may be useful for debugging + has_inline_markup = NULL ) |> dplyr::filter(is.na(outline_el) | grepl(pattern, outline_el, ignore.case = TRUE)) } diff --git a/R/proj-list.R b/R/proj-list.R index ab2a6bb..acf3168 100644 --- a/R/proj-list.R +++ b/R/proj-list.R @@ -58,10 +58,16 @@ proj_file <- function(file = NULL, proj = NULL, pattern = NULL) { if (fs::is_file(file)) { file_outline(path = file) open_rs_doc(file) + return(invisible(file)) } proj <- proj %||% proj_get2() proj_path <- proj_list(proj) - + file_path <- fs::path(proj_path, file) + if (fs::is_file(file_path)) { + file_outline(path = file_path) + open_rs_doc(file_path) + return(invisible(file_path)) + } file_exts <- c("R", "qmd", "Rmd", "md", "Rmarkdown") file_exts_regex <- paste0("*.", file_exts, "$", collapse = "|") possible_files <- fs::dir_ls(proj_path, regexp = file_exts_regex, recurse = TRUE) diff --git a/R/rename.R b/R/rename.R index 3f82e8f..1352692 100644 --- a/R/rename.R +++ b/R/rename.R @@ -71,7 +71,7 @@ rename_files2 <- function(old, # renaming should only happen in tests or interactive sessions if (action == "rename" && !(rlang::is_interactive() || identical(Sys.getenv("TESTTHAT"), "true"))) { - cli::cli_abort(c("Should only rename files in interactive sessions (or in tests)")) + cli::cli_abort("Should only rename files in interactive sessions.") } is_git <- !isFALSE(tryCatch(rprojroot::find_root_file(criterion = rprojroot::criteria$is_vcs_root), error = function(e) FALSE)) @@ -95,7 +95,7 @@ rename_files2 <- function(old, # remove project name from conflicts. related_files <- stringr::str_subset(related_files, proj_name, negate = TRUE) if (length(related_files) > 0) { - # maybe would need to normalize path. + # TODO verify if path should be normalized. cli::cli_warn(c( "Other files have a similar pattern", "See {.file {related_files}}", @@ -124,7 +124,7 @@ rename_files2 <- function(old, if (renaming_strategy == "object_names") { # Create regex = replace kebab-case by snake_case to verify object names # Then the regex is the union of these. - regex_friendly <- c(basename_remove_ext(old), stringr::str_replace_all(basename_remove_ext(old), "-", "_")) + regex_friendly <- c(basename_remove_ext(old), gsub("-", "_", basename_remove_ext(old), fixed = TRUE)) regex_friendly <- paste0(unique(regex_friendly), collapse = "|") } else { regex_friendly <- ifelse(renaming_strategy %in% c("object_names"), basename_remove_ext(old), old) @@ -216,7 +216,7 @@ compute_conflicts_regex <- function(file, renaming_strategy) { file_name_base <- basename_remove_ext(file) - object_snake_from_file_kebab <- stringr::str_replace_all(file_name_base, "-", "_") + object_snake_from_file_kebab <- gsub("-", "_", file_name_base, fixed = TRUE) if (renaming_strategy == "object_names") { # dat/file-name.csv|file_name diff --git a/R/screenshot.R b/R/screenshot.R index c4c8dbc..71e3106 100644 --- a/R/screenshot.R +++ b/R/screenshot.R @@ -1,198 +1,198 @@ -#' Save the current image in clipboard to png in your active directory -#' -#' @description -#' The screenshot will be saved as `.png` to a directory following these rules -#' 1. In a regular RStudio project (or a Quarto book), it will be saved to a `images/` directory -#' 2. In a package project, it will be saved in a `man/figures` directory -#' 3. In a Quarto Blog project, it will save in the current post's folder. -#' 4. You can always override these defaults by setting `dir` -#' -#' After using the shortcut Win + Shift + S, you can call this function! -#' -#' @details -#' If no file name is supplied, a file named `image0*.png` will be created. -#' The function then prompts you to rename the file with a more expressive name. -#' It will continue the numbering if a file named image exists. -#' -#' Still have to validate if it works on macOS, as it is not clear whether the -#' image goes to the clipboard by default -#' -#' The maximum number of images in a folder is 99. (only padding 2), should be enough. -#' -#' You should not be able to overwrite a screenshot with a generic name, only a -#' named one as it is possible you may require to retake your screenshot. -#' @param file A file name, ideally `-` (kebab-case). (extension ignored) (optional, default is `image.png`) -#' @param proj A project name -#' @param dir A directory (optional), to override the directory rules mentioned in the description. inside `proj`. -#' @return The full image path, invisibly. -#' @export -#' @examples -#' if (FALSE) { -#' # Add an image to the clipboard -#' # Run the following -#' screenshot(file = "my-new-image") -#' } -#' -screenshot <- function(file = NULL, proj = proj_get(), dir = NULL) { - # https://z3tt.github.io/graphic-design-ggplot2/tips-to-improve-your-ggplot-workflow.html#save-ggplot-output-with-the-correct-dimensions - # Could wrap ggsave also - - if (!rlang::is_interactive()) { - cli::cli_warn("Remove {.fn reuseme::screenshot} from scripts. It is only meant to be used interactively.") - return(invisible()) - } - - - check_string(file, allow_null = TRUE) - is_active_proj <- identical(proj, proj_get2()) - - proj_path <- proj_list(proj) - - - if (!rstudioapi::isAvailable()) { - cli::cli_warn("This feature may not work as excepted outside RStudio.") - } - - # Making dir a full path if not in active project. - if (!is.null(dir)) { - dir_rel <- dir - if (!is_active_proj) { - dir <- fs::path(proj_path, dir_rel) - } - - if (!fs::is_dir(dir) || !fs::dir_exists(dir)) { - cli::cli_abort(c(x = "{.arg dir} must be {.code NULL} or a valid directory within {.arg proj}.")) - } - } - - img_dir <- if (!is.null(dir)) { - if (is_active_proj) { - dir - } else { - fs::path(proj_path, dir_rel) - } - } else if (is_pkg(proj_path)) { - "man/figures" - } else if (is_quarto_blog(proj_path)) { - if (is_active_proj) { - get_active_qmd_post(base_path = proj_path) - } else { - cli::cli_abort(c("You are trying to add a screenshot to a Quarto blog.", "Either open the RStudio project or supply {.arg dir} to write a screenshot in the directory.")) - } - } else if (is_active_proj) { - "images" - } else if (!is_active_proj && !is_pkg(proj_path)) { - fs::path(proj_path, "images") - } else { - cli::cli_abort("Not a supported case.") - } - - img_dir_rel <- fs::path_rel(img_dir, start = proj_path) - - - - if (is_quarto_blog(proj_path)) { - file_index_qmd <- fs::path(img_dir, "index.qmd") - if (!fs::file_exists(file_index_qmd)) { - cli::cli_abort("In a Quarto blog, {.arg dir} must be a relative path to a Quarto Post. i.e. index.qmd.") - } - } - - if (!fs::dir_exists(img_dir)) { - cli::cli_abort(c( - x = "The directory where we want to save the image, {img_dir} doesn't exist.", - i = "Run {.run fs::dir_create(\"{img_dir}\")} to create it.", - "Then, rerun {.fun reuseme::screenshot} to save the screenshot" - )) - } - - is_generic_file_name <- is.null(file) - file <- file %||% "image" - file <- fs::path_ext_remove(file) - - if (file == "image") { - files_named_image <- fs::dir_ls( - path = img_dir, - type = "file", - regexp = "image.+png", - recurse = FALSE - ) - - if (rlang::has_length(files_named_image)) { - increment_val <- stringr::str_extract(files_named_image, "image-(\\d{2})", group = 1) - increment <- max(as.numeric(increment_val)) - } else { - increment <- 0 - } - - file <- glue::glue("image-{stringr::str_pad(increment + 1, width = 2, pad = '0')}") - } - img_file <- fs::path(file, ext = "png") - - img_path <- fs::path(img_dir, img_file) - - if (fs::file_exists(img_path) && is_generic_file_name) { - cli::cli_abort("You cannot overrite a screenshot. Please change `file`") - } - - rlang::check_installed("magick") - screen_shot <- tryCatch( - magick::image_read("clipboard:"), - error = function(e) { - cli::cli_abort( - c("x" = "The clipboard must contain an image."), - parent = NA - ) - } - ) - - magick::image_write( - image = screen_shot, - path = img_path, - format = "png", - comment = "screenshot" - ) - - img_path_chr <- as.character(img_path) - img_path_rel_chr <- as.character(fs::path_rel(img_path, proj_path)) - img_file_chr <- as.character(img_file) - img_dir_rel_chr <- as.character(img_dir_rel) - img_dir_chr <- as.character(img_dir) - proj_chr <- as.character(proj) - change_project_command <- "[{proj_chr}](reuseme::proj_switch('{proj_chr}'))" - - bullets <- if (is_active_proj) { - if (length(fs::dir_ls(".", regexp = "qmd|Rmd")) > 0) { - "Use with Quarto, Rmd (source mode) with" - } - } else { - "Use with Quarto, Rmd (source mode) in {.run [{proj_chr}](reuseme::proj_switch('{proj_chr}'))}" - } - - if (is_quarto_blog(proj_path)) { - bullets <- c( - bullets, - # cli bug r-lib/cli#683 - '![]({fs::path_file(img_path_chr)}){{fig-alt="" width="70%"}}' - ) - } else { - bullets <- c( - bullets, - '![]({img_path_rel_chr}){{fig-alt="" width="70%"}}' - ) - } - - - - if (is_generic_file_name) { - bullets <- c( - bullets, - "i" = "Consider using a more precise name", - "reuseme::rename_files2('{img_path_chr}', '{img_dir_chr}/better-name.png', warn_conflicts = 'none')", - "i" = "See {.help reuseme::rename_files2} for details." - ) - } - - cli::cli_inform(bullets) - invisible(img_path_rel_chr) -} +#' Save the current image in clipboard to png in your active directory +#' +#' @description +#' The screenshot will be saved as `.png` to a directory following these rules +#' 1. In a regular RStudio project (or a Quarto book), it will be saved to a `images/` directory +#' 2. In a package project, it will be saved in a `man/figures` directory +#' 3. In a Quarto Blog project, it will save in the current post's folder. +#' 4. You can always override these defaults by setting `dir` +#' +#' After using the shortcut Win + Shift + S, you can call this function! +#' +#' @details +#' If no file name is supplied, a file named `image0*.png` will be created. +#' The function then prompts you to rename the file with a more expressive name. +#' It will continue the numbering if a file named image exists. +#' +#' Still have to validate if it works on macOS, as it is not clear whether the +#' image goes to the clipboard by default +#' +#' The maximum number of images in a folder is 99. (only padding 2), should be enough. +#' +#' You should not be able to overwrite a screenshot with a generic name, only a +#' named one as it is possible you may require to retake your screenshot. +#' @param file A file name, ideally `-` (kebab-case). (extension ignored) (optional, default is `image.png`) +#' @param proj A project name +#' @param dir A directory (optional), to override the directory rules mentioned in the description. inside `proj`. +#' @return The full image path, invisibly. +#' @export +#' @examples +#' if (FALSE) { +#' # Add an image to the clipboard +#' # Run the following +#' screenshot(file = "my-new-image") +#' } +#' +screenshot <- function(file = NULL, proj = proj_get(), dir = NULL) { + # https://z3tt.github.io/graphic-design-ggplot2/tips-to-improve-your-ggplot-workflow.html#save-ggplot-output-with-the-correct-dimensions + # Could wrap ggsave also + + if (!rlang::is_interactive()) { + cli::cli_warn("Remove {.fn reuseme::screenshot} from scripts. It is only meant to be used interactively.") + return(invisible()) + } + + + check_string(file, allow_null = TRUE) + is_active_proj <- identical(proj, proj_get2()) + + proj_path <- proj_list(proj) + + + if (!is_rstudio()) { + cli::cli_warn("This feature may not work as excepted outside RStudio.") + } + + # Making dir a full path if not in active project. + if (!is.null(dir)) { + dir_rel <- dir + if (!is_active_proj) { + dir <- fs::path(proj_path, dir_rel) + } + + if (!fs::is_dir(dir) || !fs::dir_exists(dir)) { + cli::cli_abort(c(x = "{.arg dir} must be {.code NULL} or a valid directory within {.arg proj}.")) + } + } + + img_dir <- if (!is.null(dir)) { + if (is_active_proj) { + dir + } else { + fs::path(proj_path, dir_rel) + } + } else if (is_pkg(proj_path)) { + "man/figures" + } else if (is_quarto_blog(proj_path)) { + if (is_active_proj) { + get_active_qmd_post(base_path = proj_path) + } else { + cli::cli_abort(c("You are trying to add a screenshot to a Quarto blog.", "Either open the RStudio project or supply {.arg dir} to write a screenshot in the directory.")) + } + } else if (is_active_proj) { + "images" + } else if (!is_active_proj && !is_pkg(proj_path)) { + fs::path(proj_path, "images") + } else { + cli::cli_abort("Not a supported case.") + } + + img_dir_rel <- fs::path_rel(img_dir, start = proj_path) + + + + if (is_quarto_blog(proj_path)) { + file_index_qmd <- fs::path(img_dir, "index.qmd") + if (!fs::file_exists(file_index_qmd)) { + cli::cli_abort("In a Quarto blog, {.arg dir} must be a relative path to a Quarto Post. i.e. index.qmd.") + } + } + + if (!fs::dir_exists(img_dir)) { + cli::cli_abort(c( + x = "The directory where we want to save the image, {img_dir} doesn't exist.", + i = "Run {.run fs::dir_create(\"{img_dir}\")} to create it.", + "Then, rerun {.fun reuseme::screenshot} to save the screenshot" + )) + } + + is_generic_file_name <- is.null(file) + file <- file %||% "image" + file <- fs::path_ext_remove(file) + + if (file == "image") { + files_named_image <- fs::dir_ls( + path = img_dir, + type = "file", + regexp = "image.+png", + recurse = FALSE + ) + + if (rlang::has_length(files_named_image)) { + increment_val <- stringr::str_extract(files_named_image, "image-(\\d{2})", group = 1) + increment <- max(as.numeric(increment_val)) + } else { + increment <- 0 + } + + file <- glue::glue("image-{stringr::str_pad(increment + 1, width = 2, pad = '0')}") + } + img_file <- fs::path(file, ext = "png") + + img_path <- fs::path(img_dir, img_file) + + if (fs::file_exists(img_path) && is_generic_file_name) { + cli::cli_abort("You cannot overrite a screenshot. Please change `file`") + } + + rlang::check_installed("magick") + screen_shot <- tryCatch( + magick::image_read("clipboard:"), + error = function(e) { + cli::cli_abort( + c("x" = "The clipboard must contain an image."), + parent = NA + ) + } + ) + + magick::image_write( + image = screen_shot, + path = img_path, + format = "png", + comment = "screenshot" + ) + + img_path_chr <- as.character(img_path) + img_path_rel_chr <- as.character(fs::path_rel(img_path, proj_path)) + img_file_chr <- as.character(img_file) + img_dir_rel_chr <- as.character(img_dir_rel) + img_dir_chr <- as.character(img_dir) + proj_chr <- as.character(proj) + change_project_command <- "[{proj_chr}](reuseme::proj_switch('{proj_chr}'))" + + bullets <- if (is_active_proj) { + if (length(fs::dir_ls(".", regexp = "qmd|Rmd")) > 0) { + "Use with Quarto, Rmd (source mode) with" + } + } else { + "Use with Quarto, Rmd (source mode) in {.run [{proj_chr}](reuseme::proj_switch('{proj_chr}'))}" + } + + if (is_quarto_blog(proj_path)) { + bullets <- c( + bullets, + # cli bug r-lib/cli#683 + '![]({fs::path_file(img_path_chr)}){{fig-alt="" width="70%"}}' + ) + } else { + bullets <- c( + bullets, + '![]({img_path_rel_chr}){{fig-alt="" width="70%"}}' + ) + } + + + + if (is_generic_file_name) { + bullets <- c( + bullets, + "i" = "Consider using a more precise name", + "reuseme::rename_files2('{img_path_chr}', '{img_dir_chr}/better-name.png', warn_conflicts = 'none')", + "i" = "See {.help reuseme::rename_files2} for details." + ) + } + + cli::cli_inform(bullets) + invisible(img_path_rel_chr) +} diff --git a/R/todo.R b/R/todo.R index 650d347..5d3cd99 100644 --- a/R/todo.R +++ b/R/todo.R @@ -63,7 +63,7 @@ use_todo <- function(todo, proj = proj_get2(), code = FALSE) { #' Remove a TODO/WORK/FIXME item from a file #' -#' Function meant to be wrapped as `{.run}` hyperlinks with [file_outline()]. +#' Function meant to be wrapped as `{.run }` hyperlinks with [file_outline()]. #' It basically removes a line from a file. #' #' @param line The line number (a single integer) @@ -82,7 +82,7 @@ complete_todo <- function(line, file, regexp, rm_line = NULL) { check_number_whole(line) line_original <- line # to defer warning. - if (interactive() && rstudioapi::isAvailable()) { + if (interactive() && is_rstudio()) { rstudioapi::documentSaveAll() } warn_change_of_line <- FALSE @@ -126,9 +126,9 @@ complete_todo <- function(line, file, regexp, rm_line = NULL) { if (warn_change_of_line) { cli::cli_warn(c( x = "Could not find {.arg regexp} as expected", - "Could not find {.val {regexp}} at line {line_original}.", - i = "Has the file content changed since you ran this code?", - # needs qty for cli pluralization, but no printing + "The expected pattern {.val {regexp}} was not found at line {line_original}.", + i = "Please verify if the file content has changed or if the pattern needs adjustment.", + # needs cli::qty for pluralization, but no printing "`regexp` was detected in {cli::qty(length(regexp_detection))} line{?s} {regexp_detection}." )) } @@ -209,7 +209,7 @@ compute_path_todo <- function(todo, proj) { if (!is.na(proj_name_in_todo)) { proj <- proj_name_in_todo regex_proj_in_todo <- paste0(proj_name_in_todo, "\\:\\:", "\\s?") - todo[1] <- stringr::str_remove(todo[1], regex_proj_in_todo) + todo[1] <- sub(regex_proj_in_todo, "", todo[1]) } is_active_proj <- tryCatch( @@ -247,11 +247,11 @@ compute_path_todo <- function(todo, proj) { # accepts a single line strip_todo_line <- function(x, only_rm_tag = FALSE) { check_string(x) - if (!stringr::str_detect(x, "TODO|WORK|FIXME")) { + if (!grepl("TODO|WORK|FIXME", x)) { cli::cli_abort("Could not detect a todo tag in x") } if (only_rm_tag) { - x_new <- stringr::str_remove(x, "\\s(TODO|WORK|FIXME)") + x_new <- sub("\\s(TODO|WORK|FIXME)", "", x) } else { x_new <- stringr::str_extract(x, "([^#]+)\\#+", group = 1) if (is.na(x_new)) { diff --git a/R/utils-proj.R b/R/utils-proj.R index b32b746..945f6d3 100644 --- a/R/utils-proj.R +++ b/R/utils-proj.R @@ -112,3 +112,8 @@ get_active_qmd_post <- function(base_path = proj_get(), error_call = caller_env( fs::path_dir(relative_path) } + +# to mock. +is_rstudio <- function(v = NULL) { + rstudioapi::isAvailable(version_needed = v) +} diff --git a/README.Rmd b/README.Rmd index bd1bcd0..baf1f4b 100644 --- a/README.Rmd +++ b/README.Rmd @@ -25,7 +25,7 @@ knitr::opts_chunk$set( The goal of reuseme is to provide utility functions for project management across RStudio projects. -Sometimes, you have to manage multiple things at once, but don't have the time to do edits. +Sometimes, managing multiple projects can be challenging. reuseme also aims to simplify project management on Windows. You may need to switch quickly to a project, add things or browse a certain file if you have some replications across projects. Sometimes, it is hard to do that. reuseme also aims to help me overcome things I don't like on Windows. @@ -49,9 +49,8 @@ reuseme is adapted for a standard workflow, recommended in (find resources) - Anyone working in RStudio (recent version for hyperlink support) - Work with RStudio projects - Your RStudio projects are organized in a centralized location on your computer -- Your RStudio projects are Version controlled with git (optional, but recommended to avoid surprises! No need to be hosted on repositories like GitLab or GitHub) -- You are working on Windows (macOS is supported, but some things were designed on Windows) -- You use machine and human readable paths (i.e. no spaces, special characters) (Tip: don't hesitate to rename your files, it can take away the pain in the long run! +- Your RStudio projects are Version controlled with git (optional, but recommended for avoiding surprises! No need to be hosted on repositories like GitLab or GitHub) +- You use machine and human-readable paths (i.e. no spaces, special characters) (Tip: don't hesitate to rename your files (`reuseme::rename_files2()`), your future self will thank you! To take advantage of reuseme, it is highly recommended to set the following option in your `.Rprofile` @@ -95,7 +94,7 @@ With reuseme, just use the project name! +================================================================================+==================================================================================+======================================================================================================================================+ | Switch to project "cool-project" | `proj_switch(proj = "cool-project")` | `proj_activate(path = "C:/users/long/path/to/cool-project")` | +--------------------------------------------------------------------------------+----------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------+ -| Write a TODO item in project "cooler-project", while working in "cool-project" | `use_todo(todo = "I need to do this ASAP as possible", proj = "cooler-project")` | `usethis::write_union(path = "C:/Users/I/do/not/want/to/type/cooler-project/TODO.R", lines = "I need to do this ASAP as possible.")` | +| Write a TODO item in project "cooler-project", while working in "cool-project" | `reuseme::use_todo(todo = "I need to do this ASAP as possible", proj = "cooler-project")` | `usethis::write_union(path = "C:/Users/I/do/not/want/to/type/cooler-project/TODO.R", lines = "I need to do this ASAP as possible.")` | +--------------------------------------------------------------------------------+----------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------+ | Open pkgdown site link and see vignettes | 1. \`browse_pkg("usethis") | 1. `browse_package("usethis")` | | | 2. Click on the hyperlinks that correspond to your query | 2. Type the correct number that corresponds | @@ -128,6 +127,7 @@ Due to the growing number of criteria, regex, `file_outline()` is slowing down a ```{r} #| warning: false +#| message: false bench::mark( outline <- proj_outline() ) diff --git a/README.md b/README.md index da67599..fc63af3 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ coverage](https://codecov.io/gh/olivroy/reuseme/branch/main/graph/badge.svg)](ht The goal of reuseme is to provide utility functions for project -management across RStudio projects. Sometimes, you have to manage -multiple things at once, but don’t have the time to do edits. You may -need to switch quickly to a project, add things or browse a certain file -if you have some replications across projects. Sometimes, it is hard to -do that. reuseme also aims to help me overcome things I don’t like on -Windows. +management across RStudio projects. Sometimes, managing multiple +projects can be challenging. reuseme also aims to simplify project +management on Windows. You may need to switch quickly to a project, add +things or browse a certain file if you have some replications across +projects. Sometimes, it is hard to do that. reuseme also aims to help me +overcome things I don’t like on Windows. ## Installation @@ -45,13 +45,11 @@ resources) - Your RStudio projects are organized in a centralized location on your computer - Your RStudio projects are Version controlled with git (optional, but - recommended to avoid surprises! No need to be hosted on repositories - like GitLab or GitHub) -- You are working on Windows (macOS is supported, but some things were - designed on Windows) -- You use machine and human readable paths (i.e. no spaces, special - characters) (Tip: don’t hesitate to rename your files, it can take - away the pain in the long run! + recommended for avoiding surprises! No need to be hosted on + repositories like GitLab or GitHub) +- You use machine and human-readable paths (i.e. no spaces, special + characters) (Tip: don’t hesitate to rename your files + (`reuseme::rename_files2()`), your future self will thank you! To take advantage of reuseme, it is highly recommended to set the following option in your `.Rprofile` @@ -103,33 +101,36 @@ If you want to work across projects with [usethis](usethis.r-lib.org), you need to provide the full path to a project. With reuseme, just use the project name! -
Workflow | -reuseme | -usethis | -||||
---|---|---|---|---|---|---|
Switch to project “cool-project” | -proj_switch(proj = "cool-project") |
-proj_activate(path = "C:/users/long/path/to/cool-project") |
+Workflow+Switch to project “cool-project” |
+reuseme+
|
+usethis+
|
+|
+ | ||||||
Write a TODO item in project “cooler-project”, while working in “cool-project” | -use_todo(todo = "I need to do this ASAP as possible", proj = "cooler-project") |
-usethis::write_union(path = "C:/Users/I/do/not/want/to/type/cooler-project/TODO.R", lines = "I need to do this ASAP as possible.") |
+reuseme::use_todo(todo = "I need to do this ASAP as possible", proj = "cooler-project")
+|
+usethis::write_union(path = "C:/Users/I/do/not/want/to/type/cooler-project/TODO.R", lines = "I need to do this ASAP as possible.") |
|||
Open pkgdown site link and see vignettes |
|
+