diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 951ea85..b6f9740 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -47,7 +47,7 @@ See our guide on [how to create a great issue](https://code-review.tidyverse.org * `define_outline_criteria()` if an item shows as outline, but seems like a false positive, -* `keep_outline_element()`: if an element is **missing** from outline. +* `keep_outline_element()`: if an element is **missing** from outline, you can add the keyword "REQUIRED ELEMENT" to get an object for debugging. * `define_important_element()` if an element is important [^1] diff --git a/DESCRIPTION b/DESCRIPTION index 5026aa7..256121a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,9 +35,12 @@ Suggests: curl, gert, gt, + lightparser, magick, pillar, + roxygen2, testthat (>= 3.2.1), + tidyr, withr Config/testthat/edition: 3 Encoding: UTF-8 diff --git a/NEWS.md b/NEWS.md index d272ded..e72c348 100644 --- a/NEWS.md +++ b/NEWS.md @@ -36,6 +36,12 @@ that will passed on to `proj_list()` * `proj_list()` / `proj_switch()` no longer opens a nested project if looking for `"pkgdown"`, `"testthat"`, etc. +* `proj_outline()` was improved to work with roxygen2 and lightparser to parse file contents more consistenly. This means a slowdown, but the increased accuracy is worth it! Parsing a single file should still be pretty fast! + +* `proj_outline()` gains `exclude_tests` to exclude tests from outline + +* `proj_outline()` now detects legacy `fig.cap` in the chunk header. See `knitr::convert_chunk_headers()` for the newer approach. + * `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. diff --git a/R/outline-criteria.R b/R/outline-criteria.R index ef7dd66..2157944 100644 --- a/R/outline-criteria.R +++ b/R/outline-criteria.R @@ -15,21 +15,26 @@ extract_pkg_version <- function(x, is_news, is_heading) { #' * is test title #' * is a todo item #' * is_roxygen_line -#' * is_tab_title +#' * is_tab_plot_title #' #' @noRd -o_is_roxygen_comment <- function(x, file_ext = NULL) { + +o_is_roxygen_comment <- function(x, file_ext = NULL, is_notebook = FALSE) { if (!is.null(file_ext)) { - is_r_file <- tolower(file_ext) == "r" + is_r_file <- tolower(file_ext) == "r" & !is_notebook } else { - is_r_file <- TRUE + is_r_file <- !is_notebook } if (!any(is_r_file)) { return(FALSE) } - ifelse(rep(is_r_file, length.out = length(x)), stringr::str_starts(x, "#'\\s"), FALSE) + ifelse( + rep(is_r_file, length.out = length(x)), + grepl("^#'\\s|^#'$", x), # detect roxygen comments in R files + FALSE # not a roxy comment in Rmd files, fusen is an exception? + ) } o_is_notebook <- function(x, file, file_ext, line) { @@ -106,14 +111,23 @@ o_is_tab_plot_title <- function(x) { !stringr::str_detect(x, "expect_error|header\\(\\)|```\\{|guide_") } -o_is_section_title <- function(x, is_roxygen_comment = FALSE, is_todo_fixme = FALSE) { - is_section_title <- !is_roxygen_comment & !is_todo_fixme & stringr::str_detect(x, "^\\s{0,4}\\#+\\s+(?!\\#)") & !is_roxygen_comment # remove commented add roxygen +o_is_section_title <- function(x, is_roxygen_comment = FALSE, is_todo_fixme = FALSE, roxy_section = FALSE) { + is_section_title <- roxy_section | + (!is_roxygen_comment & !is_todo_fixme & stringr::str_detect(x, "^\\s{0,4}\\#+\\s+(?!\\#)") & !is_roxygen_comment) # remove commented add roxygen if (!any(is_section_title)) { return(is_section_title) } if (length(is_roxygen_comment) == 1) { rep(is_roxygen_comment, length.out = length(is_section_title)) } + if (length(roxy_section) == 1) { + rep(roxy_section, length.out = length(is_section_title)) + } + if (any(roxy_section)) { + x[roxy_section] <- sub("@section", "", x, fixed = TRUE) + x[roxy_section] <- sub(":$", "", x, fixed = F) + + } uninteresting_headings <- paste( "(Tidy\\s?T(uesday|emplate)|Readme|Wrangle|Devel)$|error=TRUE", "url\\{|Error before installation|unreleased|Function ID$|Function Introduced", @@ -150,14 +164,19 @@ o_is_cli_info <- function(x, is_snap_file = FALSE, file = "file") { # Add variable to outline data frame -------------------- -define_outline_criteria <- function(.data, print_todo) { +define_outline_criteria <- function(.data, exclude_todos) { + dir_common <- get_dir_common_outline(.data$file) x <- .data x$file_ext <- s_file_ext(x$file) x$is_md <- x$file_ext %in% c("qmd", "md", "Rmd", "Rmarkdown") x$is_news <- x$is_md & grepl("NEWS.md", x$file, fixed = TRUE) - x$is_md <- x$is_md & !x$is_news # treating news and other md files differently. x$is_test_file <- grepl("tests/testthat/test", x$file, fixed = TRUE) + x$is_notebook <- o_is_notebook(x = x$content, x$file, x$file_ext, x$line) + x$is_roxygen_comment <- o_is_roxygen_comment(x$content, x$file_ext, x$is_notebook) + x$content[x$is_notebook] <- sub("^#'\\s?", "", x$content[x$is_notebook]) + x$is_md <- (x$is_md | x$is_roxygen_comment | x$is_notebook) & !x$is_news # treating news and other md files differently. x$is_snap_file <- grepl("_snaps", x$file, fixed = TRUE) + x$is_roxygen_comment <- o_is_roxygen_comment(x$content, x$file_ext) if (any(x$is_roxygen_comment)) { # detect knitr notebooks @@ -176,37 +195,75 @@ define_outline_criteria <- function(.data, print_todo) { } else { x$is_notebook <- FALSE } + + should_parse_roxy_comments <- + !isFALSE(getOption("reuseme.roxy_parse", default = TRUE)) && # will not parse if option is set to FALSE + any(x$is_roxygen_comment) + if (should_parse_roxy_comments) { + # doing this created problems in tests? + if (interactive() && !is.null(dir_common) && is_rstudio()) { + # The idea is that roxygen2 may be better at getting objects if directory is changed. + # but don't bother doing this outside RStudio for now... + withr::local_dir(dir_common) + if (!fs::file_exists(x$file[1])) { + cli::cli_abort("Wrong dir done. file = {.file {x$file[1]}. dir = {.path {dir_common}}", .internal = TRUE) + } + } + rlang::check_installed(c("roxygen2", "tidyr"), "to create roxygen2 comments outline.") + files_with_roxy_comments <- unique(x[x$is_roxygen_comment, "file", drop = TRUE]) + files_with_roxy_comments <- rlang::set_names(files_with_roxy_comments, files_with_roxy_comments) + # roxygen2 messages + # TRICK purrr::safely creates an error object, while possible is better. + # Suppresss roxygen2 message, suppress callr output, suppress asciicast warnings. + invisible( + utils::capture.output( + parsed_files <- purrr::map( + files_with_roxy_comments, + purrr::possibly(\(x) roxygen2::parse_file(x, env = NULL)))) + ) |> + suppressMessages() |> + suppressWarnings() + # if roxygen2 cannot parse a file, let's just forget about it. + unparsed_files <- files_with_roxy_comments[which(is.null(parsed_files))] + # browser() + if (length(unparsed_files) > 0) { + cli::cli_inform("Could not parse roxygen comments in {.file {unparsed_files}}") + } + parsed_files <- purrr::compact(parsed_files) + processed_roxy <- join_roxy_fun(parsed_files) + outline_roxy <- define_outline_criteria_roxy(processed_roxy) + } else { + outline_roxy <- NULL + } + x <- dplyr::mutate( - x, + x |> dplyr::filter(!is_roxygen_comment), # Problematic when looking inside functions # maybe force no leading space. - # TODO strip is_cli_info in Package? only valid for EDA + # TODO strip is_cli_info in Package? only valid for EDA (currently not showcased..) is_cli_info = o_is_cli_info(content, is_snap_file, file), + # TODO long enough to be meanignful? + # doc title cannot be after line 50 of a document. is_doc_title = stringr::str_detect(content, "(?\\|]$"), - is_chunk_cap = dplyr::case_when( - is_chunk_cap & is_chunk_cap_next ~ FALSE, - dplyr::lag(is_chunk_cap_next, default = FALSE) ~ TRUE, - .default = is_chunk_cap - ), - is_chunk_cap_next = is_chunk_cap, + is_obj_caption = stringr::str_detect(content, "\\#\\|\\s{1,2}[:alpha:]{0,5}[\\-\\.]?(cap|title)[:(\\s*=)]|```\\{r.*cap\\s?\\="), is_test_name = is_test_file & o_is_test_name(content) & !o_is_generic_test(content), - is_todo_fixme = print_todo & o_is_todo_fixme(content, is_roxygen_comment) & !is_snap_file, + is_todo_fixme = !exclude_todos & o_is_todo_fixme(content) & !o_is_roxygen_comment(content, file_ext, is_notebook) & !is_snap_file, is_section_title = o_is_section_title(content, is_roxygen_comment, is_todo_fixme), pkg_version = extract_pkg_version(content, is_news, is_section_title), is_section_title_source = is_section_title & stringr::str_detect(content, "[-\\=]{3,}|^\\#'") & stringr::str_detect(content, "[:alpha:]"), - n_leading_hash = nchar(stringr::str_extract(content, "\\#+")), + n_leading_hash = nchar(stringr::str_extract(content, "\\#+(?!\\|)")), # don't count hashpipe 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, + # roxygen2 title block + is_object_title = FALSE, + tag = NA_character_, + topic = NA_character_, is_cross_ref = stringr::str_detect(content, "docs_(links|add.+)?\\(.") & !stringr::str_detect(content, "@param|\\{\\."), 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, @@ -217,7 +274,73 @@ define_outline_criteria <- function(.data, print_todo) { line == 1 | !nzchar(dplyr::lead(content, default = "")) & !nzchar(dplyr::lag(content)), .by = "file" ) + # browser() + res <- dplyr::bind_rows(x, outline_roxy) + res <- dplyr::filter( + res, + content != "NULL" + ) + res <- dplyr::arrange(res, .data$file, .data$line) + #res$is_object_title[res$is_doc_title] <- FALSE + res +} + + +define_outline_criteria_roxy <- function(x) { + # TODO merge with define_outline_criteria + if (rlang::is_atomic(x)) { + # in tests, not interactively, got something bizzare + cli::cli_warn("x is {.obj_type_friendly {x}}.") + if (length(x) == 0) { + return(NULL) + } + } + x$is_md <- x$tag %in% c("subsection", "details", "description", "section") + # short topics are likely placeholders. + x$is_object_title <- x$tag == "title" & nchar(x$content) > 4 + x$line <- as.integer(x$line) + x$file_ext <- "R" + # x$content <- paste0("#' ", x$content) # maybe not? + x$is_news <- FALSE + x$is_roxygen_comment <- TRUE + x$is_test_file <- FALSE + x$is_snap_file <- FALSE + x$before_and_after_empty <- TRUE + x$is_section_title <- + (x$tag %in% c("section", "subsection") & o_is_section_title(x$content, roxy_section = TRUE)) | + (x$tag %in% c("details", "description") & stringr::str_detect(x$content, "#\\s")) + x$is_section_title_source <- x$is_section_title + x$is_obj_caption <- FALSE + x$is_test_name <- FALSE + x$pkg_version <- NA_character_ + # a family or concept can be seen as a plot subtitle? + x$is_tab_or_plot_title <- x$tag %in% c("family", "concept") + x$is_cli_info <- FALSE + x$is_cross_ref <- FALSE + x$is_function_def <- FALSE + x$is_todo_fixme <- FALSE + x$is_notebook <- FALSE + x$is_doc_title <- FALSE + #x$is_doc_title <- x$line == 1 & x$tag == "title" + x$n_leading_hash <- nchar(stringr::str_extract(x$content, "\\#+")) + x$n_leading_hash <- dplyr::case_when( + x$n_leading_hash > 0 ~ x$n_leading_hash, + # give second importance to doc sections.. + x$tag == "section" & x$is_section_title_source ~ 2, + x$tag == "subsection" & x$is_section_title_source ~ 3, + .default = 0 + ) + x$content <- dplyr::case_when( + !x$is_section_title ~ x$content, + # : removed from section tag in join_roxy_fun() + # code section may not be that interesting.. + x$tag == "section" ~ paste0("## ", x$content), + x$tag == "subsection" ~ paste0("### ", x$content), + .default = x$content + ) + x$is_second_level_heading_or_more <- ((x$is_section_title_source | x$is_section_title) & x$n_leading_hash > 1) + # x$has_inline_markup <- FALSE # let's not mess with inline markup x } -# it is {.file R/outline.R} ------ +# it is {.file R/outline.R} or {.file R/outline-roxy.R} ------ diff --git a/R/outline-roxy.R b/R/outline-roxy.R new file mode 100644 index 0000000..c9cd6a8 --- /dev/null +++ b/R/outline-roxy.R @@ -0,0 +1,288 @@ +#' Extract roxygen tag +#' +#' Tell me what this does +#' +#' # Section to extract +#' +#' Well this is a section +#' +#' @noRd +#' @param file A list of roxy blocks +#' @returns A named list with name = file:line, and element is the section title +#' @examples +#' extract_roxygen_tag_location(tag = "title") +extract_roxygen_tag_location <- function(file, tag) { + # suppressMessages(aa <- roxygen2::parse_file(file)) + # browser() + aa <- file + pos <- purrr::map(aa, \(x) roxygen2::block_get_tags(x, tags = tag)) + # browser() + if (all(lengths(pos) == 0L)) { + return(character(0L)) + } + aa <- aa[lengths(pos) > 0L] + pos <- pos[lengths(pos) > 0L] + objects <- purrr::map( + aa, + \(x) { + if (!is.null(x$object$topic)) { + return(x$object$topic) + } + object_call <- as.character(x$call) + if (length(object_call) == 1) { + return(object_call) + } + if (length(object_call) > 1) { + return(paste0(object_call[2], "()")) + } + NULL + } + ) + if (any(lengths(objects) == 0)) { + name_tag <- purrr::map( + aa, + \(x) roxygen2::block_get_tag_value(x, "name") + ) + for (i in seq_along(objects)) { + if (is.null(objects[[i]])) { + if (!is.null(name_tag[[i]])) { + objects[[i]] <- name_tag[[i]] + } else { + objects[[i]] <- NA_character_ + } + } + } + if (any(lengths(objects) == 0)) { + # should not happen. I chose NA instead. + cli::cli_abort("Could not resolve object or topic names.") + } + } + + # double object name + for (i in seq_along(pos)) { + l <- length(pos[[i]]) + if (l > 1) { + # to repeat object name to be same length as `pos` + objects[[i]] <- as.list(rep(objects[[i]][1], length.out = l)) + } + } + # Unnest to make it easier. + pos <- purrr::list_flatten(pos) + objects <- purrr::list_flatten(objects) + if (length(objects) != length(pos)) { + print(objects) + print(pos) + cli::cli_abort( + c( + "Could not resolve pos and objects to be the same length.", + "pos = {length(pos)}, objects = {length(objects)}." + ), + .internal = TRUE + ) + } + + pos <- purrr::set_names(pos, pos$file) + + val <- withCallingHandlers( + purrr::map2(pos, objects, \(x, obj_name) { + el <- x$val + el_has_names <- !is.null(names(el)) + + if (length(el) == 1 && !el_has_names) { + el <- paste0( + el, "____", obj_name + ) + names(el) <- x$line + return(el) + } + if (tag %in% c("description", "details") && !el_has_names) { + # TODO when stable delete + # print(x$val) + # print(el_has_names) + # cli::cli_inform("return early (no headings)") + return(NULL) + } + # use raw instead + lines <- stringr::str_split_1(x$raw, "\n") + # browser() + keep <- which(o_is_section_title(lines)) + + if (length(keep) == 0L) { + # TODO Delete when stable debugging + # cli::cli_inform(" No section title detected") + return(NULL) + } + # line position. + line_pos <- x$line + seq_along(lines) - 1L + final_lines_to_include <- lines[keep] + # Will not make this transformation and will consider roxygen comments to be + # final_lines_to_include <- sub("^#+\\s", "", final_lines_to_include) + + final_lines_to_include <- paste0(final_lines_to_include, "____", obj_name) + names(final_lines_to_include) <- line_pos[keep] + # TODO Delete when stable for debugging + # if (length(final_lines_to_include) != 1) { + # cli::cli_warn("el resulted to {.val {final_lines_to_include}}", "using first element for now") + # } + final_lines_to_include + }), + error = function(e) { + cli::cli_abort( + "For tag = {tag}, obj_name = {objects}, wrong size, should be {length(pos)}, not {length(objects)}.", + parent = e + ) + } + ) + + # rlang::set_names(val, nam) + # merge line number and file name + # I wonder if purrr make it easy to do tidyverse/purrr#1064 + # list(x = c(el1 = 1), x = c(el2 = 2, el3 = 3)) + #> list(x = c(el1 = 1, el2 = 2, el3 = 3)) + val <- val |> purrr::compact() + + if (FALSE) { + val <- unlist(val) + names(val) <- stringr::str_replace(names(val), "\\.(\\d+)$", ":\\1") + } else { + # purrr::list_flatten( + # name_spec = "{outer}:{inner}" + # ) + val <- vctrs::list_unchop( + val, + name_spec = "{outer}:{inner}", + ptype = "character" + ) + } + + + # hack to keep tag + if (length(val) > 0) { + names(val) <- paste0(names(val), "____", tag) + } + val +} + +join_roxy_fun <- function(file) { + # Assuming that only @keywords internal is used, when keywords is specified. + # Will probably have to handle other cases, but this is not recommended. + # https://roxygen2.r-lib.org/reference/tags-index-crossref.html + parsed_files <- purrr::map( + file, + # discard if noRd or has keywords. + \(x) purrr::discard(x, \(y) roxygen2::block_has_tags(y, c("keywords", "noRd"))) + ) + # TODO exclude S3 methods + # Return early if no roxy tags + if (length(parsed_files) == 0) { + return(character(0L)) + } + if (is.null(names(parsed_files))) { + # browser() + parsed_files <- parsed_files |> purrr::set_names(purrr::map_chr(parsed_files, \(x) x$file)) + # cli::cli_abort("parsed files must be named at this point.") + } + # parsed_files <- set_names(parsed_files, \(x)) + titles_list <- purrr::map(parsed_files, \(x) extract_roxygen_tag_location(x, tag = "title")) + + section_list <- purrr::map(parsed_files, \(x) extract_roxygen_tag_location(x, tag = "section")) + subsection_list <- purrr::map(parsed_files, \(x) extract_roxygen_tag_location(x, tag = "subsection")) + + desc_list <- purrr::map(parsed_files, \(x) extract_roxygen_tag_location(x, tag = "description")) + + details_list <- purrr::map(parsed_files, \(x) extract_roxygen_tag_location(x, tag = "details")) + + family_list <- purrr::map(parsed_files, \(x) extract_roxygen_tag_location(x, tag = "family")) + concept_list <- purrr::map(parsed_files, \(x) extract_roxygen_tag_location(x, tag = "concept")) + roxy_parsed <- vctrs::vec_c( + titles_list, + section_list, + subsection_list, + desc_list, + details_list, + family_list, + concept_list # , + # .name_spec = "{outer}:::::{inner}", + ) |> + vctrs::list_unchop( + name_spec = "{outer}.....{inner}" + ) |> + tibble::enframe() |> + tidyr::separate_wider_delim( + cols = name, + names = c("file_line", "tag"), + delim = "____" + ) + + roxy_parsed <- roxy_parsed |> + tidyr::separate_wider_delim( + cols = value, + delim = "____", + names = c("content", "topic"), + ) + if (!all(grepl("\\.{5}", roxy_parsed$file_line, fixed = FALSE))) { + problems <- which(!grepl("\\.{5}", roxy_parsed$file_line, fixed = FALSE)) + # rowser() + # roxy_parsed + cli::cli_abort("Malformed file line at {problems}.") + } + roxy_parsed <- roxy_parsed |> + tidyr::separate_wider_delim( + file_line, + delim = ".....", + names = c("file", "line") + ) + + if (nrow(roxy_parsed) == 0) { + return(roxy_parsed) + } + roxy_parsed1 <- roxy_parsed |> + dplyr::relocate( + file, topic, content, line, tag + ) |> + dplyr::mutate(id = dplyr::row_number()) |> + dplyr::mutate(content = dplyr::case_when( + # only keep the first line of section, subsection, family and concept tags. + tag %in% c("family", "concept") ~ stringr::str_extract(content, "^(.+)(\n)?", group = 1), + # Remove code blocks... + tag %in% c("details", "description") ~ stringr::str_remove_all(content, "```[^`]+```"), + # Also remove : from section + tag %in% c("section", "subsection") ~ stringr::str_extract(content, "^(.+)\\s?\\:\\s?\n?", group = 1), + .default = content + )) |> + tidyr::separate_longer_delim(content, delim = "\n") + + roxy_parsed1$topic <- dplyr::na_if(roxy_parsed1$topic, "NA") + r <- roxy_parsed1 |> + dplyr::mutate( + n = dplyr::n(), + # error if something is length 0. + line = seq(from = line[1], length.out = n[1], by = 1), + .by = id, + content = dplyr::case_when( + # remove markup. + # avoid \code{} in tags. r-lib/roxygen2#1618 + tag %in% c("title", "section", "subsection") ~ stringr::str_remove_all(content, "\\}+|\\\\+[:alpha:]+\\{+|\\{$"), + .default = content + ) + ) |> + dplyr::filter(nzchar(content), !stringr::str_detect(content, "`r\\s")) |> + dplyr::select(-id, -n) + # remove link... + r$content <- stringr::str_remove_all(r$content, "\\\\[^\\\\]+\\]\\{") + # FIXME escape markup see next line + # To test it add a fix tag to next line and try to figure it out... + # to fix figure out why #' @section Escaping `{` and `}` : isn't parsing. Workaround to remove it. + r |> dplyr::filter(!grepl("^Escaping", content)) +} + +# helper for interactive checking ----------- + + +active_doc_parse <- function(doc = active_rs_doc()) { + doc <- purrr::set_names(doc) + parsed <- purrr::map(doc, \(x) roxygen2::parse_file(x, env = NULL)) + parsed |> + join_roxy_fun() |> + define_outline_criteria_roxy() +} diff --git a/R/outline.R b/R/outline.R index 85873f5..9e9b18d 100644 --- a/R/outline.R +++ b/R/outline.R @@ -13,7 +13,7 @@ #' * `TODO` items #' * Parse cli hyperlinks #' * Plot or table titles -#' * FIgures caption in Quarto documents (limited support for multiline caption currently) +#' * Figures caption in Quarto documents #' * test names #' * Indicator of recent modification #' * Colored output for @@ -30,8 +30,7 @@ #' In `proj_outline()`, `path` accepts project names, see [proj_list()] for how to #' set up reuseme to regognize your projects' locations. #' -#' The parser is very opinionated and is not very robust as it is based on regexps. -#' For a better file parser, explore other options, like [lightparser](https://thinkr-open.github.io/lightparser/) for Quarto, `{roxygen2}` +#' The parser is opinionated and based on lightparser, roxygen2 and regexps. #' #' Will show TODO items and will offer a link to [mark them as #' complete][complete_todo()]. @@ -44,13 +43,15 @@ #' Defaults to the [active file][active_rs_doc()], project or directory. #' @param pattern A string or regex to search for in the outline. If #' specified, will search only for elements matching this regular expression. -#' The print method will show the document title for context. Previously `regex_outline` -#' @param print_todo Should include TODOs in the file outline? If `FALSE`, will +#' The print method will show the document title for context. Previously `regex_outline`. +#' @param exclude_tests Should tests be displayed? (Not sure if this argument will stay, Will have to think) +#' @param exclude_todos Should include TODOs in the file outline? If `FALSE`, will #' print a less verbose output with sections. #' @param alpha Whether to show in alphabetical order #' @param dir_tree If `TRUE`, will print the [fs::dir_tree()] or non-R files in #' the directory #' @param recent_only Show outline for recent files +#' @param print_todo `r lifecycle::badge("deprecated")`. Use `exclude_todos` instead. #' @inheritParams fs::dir_ls #' @returns A `outline_report` object that contains the information. Inherits #' `tbl_df`. @@ -62,7 +63,7 @@ #' file_outline(file) #' #' # Remove todo items -#' file_outline(file, print_todo = FALSE, alpha = TRUE) +#' file_outline(file, exclude_todos = TRUE, alpha = TRUE) #' #' # interact with data frame #' file_outline(file) |> dplyr::as_tibble() @@ -82,9 +83,10 @@ NULL file_outline <- function(path = active_rs_doc(), pattern = NULL, alpha = FALSE, - print_todo = TRUE, - recent_only = FALSE) { - # To contribute to this function, take a look at .github/CONTRIBUTING + exclude_todos = FALSE, + recent_only = FALSE, + print_todo = deprecated()) { + # To contribute to this function, take a look at .github/CONTRIBUTING.md check_string(pattern, allow_null = TRUE) if (length(path) == 1L && rlang::is_interactive() && is_rstudio()) { @@ -98,6 +100,17 @@ file_outline <- function(path = active_rs_doc(), cli::cli_abort("No path specified.") } + if (lifecycle::is_present(print_todo)) { + exclude_todos <- !print_todo + lifecycle::deprecate_warn( + when = "0.0.3", + what = "file_outline(print_todo)", + with = "file_outline(exclude_todos)" + ) + } + + # active_rs_doc() returns `NULL` if the active document is unsaved. + is_saved_doc <- !is.null(path) if (is_saved_doc) { # little help temporarily if (any(stringr::str_detect(path, "~/rrr|~/Requests"))) { @@ -130,7 +143,7 @@ file_outline <- function(path = active_rs_doc(), # After this point we have validated that paths exist. - file_sections00 <- define_outline_criteria(file_content, print_todo = print_todo) + file_sections00 <- define_outline_criteria(file_content, exclude_todos = exclude_todos) # filter for interesting items. # Also scrub duplicated items, as they are likely to be uninteresting. @@ -199,10 +212,8 @@ file_outline <- function(path = active_rs_doc(), } #' @rdname outline #' @export -proj_outline <- function(path = active_rs_proj(), pattern = NULL, dir_tree = FALSE, alpha = FALSE, recent_only = FALSE) { - check_proj(path, allow_null = TRUE) +proj_outline <- function(path = active_rs_proj(), pattern = NULL, exclude_tests = FALSE, exclude_todos = FALSE, dir_tree = FALSE, alpha = FALSE, recent_only = FALSE) { path_proj <- proj_list(path) - if (!rlang::has_length(path_proj, 1)) { cli::cli_abort("Cannot process more than one project/directory at once.") } @@ -224,6 +235,8 @@ proj_outline <- function(path = active_rs_proj(), pattern = NULL, dir_tree = FAL dir_outline( path = path_proj, pattern = pattern, + exclude_tests = exclude_tests, + exclude_todos = exclude_todos, dir_tree = dir_tree, alpha = alpha, recurse = TRUE @@ -231,7 +244,7 @@ proj_outline <- function(path = active_rs_proj(), pattern = NULL, dir_tree = FAL } #' @rdname outline #' @export -dir_outline <- function(path = ".", pattern = NULL, dir_tree = FALSE, alpha = FALSE, recent_only = FALSE, recurse = FALSE) { +dir_outline <- function(path = ".", pattern = NULL, exclude_tests = FALSE, exclude_todos = FALSE, dir_tree = FALSE, alpha = FALSE, recent_only = FALSE, recurse = FALSE) { dir <- fs::path_real(path) file_exts <- c("R", "RProfile", "qmd", "Rmd", "md", "Rmarkdown") file_exts_regex <- paste0("*.", file_exts, "$", collapse = "|") @@ -249,6 +262,14 @@ dir_outline <- function(path = ".", pattern = NULL, dir_tree = FALSE, alpha = FA file_list_to_outline <- exclude_example_files(file_list_to_outline) } + if (exclude_tests) { + file_list_to_outline <- fs::path_filter( + file_list_to_outline, + regexp = "tests/", + invert = TRUE + ) + } + # TODO expand this to apply to most generated files if (any(grepl("README.Rmd", file_list_to_outline))) { file_list_to_outline <- stringr::str_subset(file_list_to_outline, "README.md", negate = TRUE) @@ -278,7 +299,7 @@ dir_outline <- function(path = ".", pattern = NULL, dir_tree = FALSE, alpha = FA invert = TRUE ) } - file_outline(path = file_list_to_outline, pattern = pattern, alpha = alpha, recent_only = recent_only) + file_outline(path = file_list_to_outline, pattern = pattern, exclude_todos = exclude_todos, alpha = alpha, recent_only = recent_only) } exclude_example_files <- function(path) { @@ -405,10 +426,17 @@ print.outline_report <- function(x, ...) { # add first line to title and remove has_title <- !is.na(summary_links_files$first_line[[i]]) if (has_title) { - title_el <- cli::format_inline(escape_markup(summary_links_files$first_line_el[[i]])) + title_el <- withCallingHandlers( + cli::format_inline(escape_markup(summary_links_files$first_line_el[[i]])), + error = function(e) { + thing <- summary_links_files$first_line_el[[i]] + print(thing) + print(escape_markup(thing)) + cli::cli_abort("Failed to parse in first line of file {.file {summary_links_files$file[[i]]}}.", parent = e) + } + ) base_name <- c(base_name, " ", title_el) } - # TRICK need tryCatch when doing something, withCallingHandlers when only rethrowing? tryCatch( cli::cli_h3(base_name), @@ -459,7 +487,7 @@ construct_outline_link <- function(.data) { # to create `complete_todo()` links (only with active doc + is_todo_fixme) (and truncate if necessary) condition_to_truncate = !is.na(outline_el) & !has_title_el & (complete_todo_link) & is_saved_doc & !has_inline_markup, # Truncate todo items, subtitles - condition_to_truncate2 = !is.na(outline_el) & !has_title_el & (is_todo_fixme & !complete_todo_link) & (is_second_level_heading_or_more | is_subtitle) & is_saved_doc & !has_inline_markup + condition_to_truncate2 = !is.na(outline_el) & !has_title_el & (is_todo_fixme & !complete_todo_link) & (is_second_level_heading_or_more | is_subtitle | is_obj_caption) & is_saved_doc & !has_inline_markup ) # r-lib/cli#627, add a dot before and at the end (Only in RStudio before 2023.12) .data$outline_el2 <- NA_character_ @@ -519,12 +547,19 @@ construct_outline_link <- function(.data) { cli::cli_abort("Define this in {.fn define_important_element}", .internal = TRUE) } + # Tweak n_leasing hash for todos or fixme.. + .data$n_leading_hash <- dplyr::case_when( + .data$is_todo_fixme ~ dplyr::lead(.data$n_leading_hash, default = 0) + 1, + .default = .data$n_leading_hash + ) + .data$leading_space <- purrr::map_chr(.data$n_leading_hash, \(x) paste(rep(" ", length.out = max(min(x - 1, 1), 0)), collapse = "")) dplyr::mutate(.data, # link_rs_api = paste0("{.run [", outline_el, "](reuseme::open_rs_doc('", file_path, "', line = ", line, "))}"), link_rs_api = dplyr::case_when( is.na(outline_el2) ~ NA_character_, !is_saved_doc ~ paste0("line ", line, " -", outline_el2), rs_avail_file_link ~ paste0( + leading_space, "{cli::style_hyperlink(", style_fun, ', "', paste0("file://", file_path), '", params = list(line = ', line, ", col = 1))} ", outline_el2 ), @@ -555,6 +590,10 @@ keep_outline_element <- function(.data) { } else { versions_to_drop <- character(0L) } + # browser() + # For debugging. + needed_elements <- which(grepl("REQUIRED ELEMENT", .data$content, fixed = TRUE) & !grepl("grep|keyword", .data$content, fixed = FALSE)) + dat <- dplyr::filter( .data, (is_news & ( @@ -564,13 +603,20 @@ keep_outline_element <- function(.data) { # still regular comments in .md files # what to keep in .md docs - (is_md & (is_chunk_cap | (is_section_title & before_and_after_empty))) | + (is_md & (is_obj_caption | (is_section_title & before_and_after_empty))) | # 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_doc_title # | 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_object_title | is_doc_title # | is_cli_info # TODO reanable cli info ) + dat$simplify_news <- NULL + if (length(needed_elements) != length(grep("REQUIRED ELEMENT", dat$content, fixed = TRUE))) { + zz <<- dplyr::slice(.data, needed_elements) + cli::cli_abort( + "Debugging mode. An important element is absent from the outline. Review filters, regex detection etc." + ) + } dat # Remove duplicate outline elements @@ -633,15 +679,20 @@ display_outline_element <- function(.data) { } 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) + if (any(x$is_obj_caption)) { + x$outline_el[x$is_obj_caption] <- extract_object_captions(x$file[x$is_obj_caption]) + } x <- dplyr::mutate( x, outline_el = dplyr::case_when( is_todo_fixme ~ stringr::str_extract(outline_el, "(TODO.+)|(FIXME.+)|(WORK.+)|(BOOK.+)"), is_test_name ~ stringr::str_extract(outline_el, "(test_that|describe)\\(['\"](.+)['\"],\\s?\\{", group = 2), is_cli_info ~ stringr::str_extract(outline_el, "[\"'](.{5,})[\"']") |> stringr::str_remove_all("\""), + # Add related topic if available + tag == "title" & !is.na(topic) ~ paste0(outline_el, " [", topic, "]"), + # family or concept! + is_tab_or_plot_title & !is.na(tag) ~ outline_el, 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 @@ -655,9 +706,41 @@ display_outline_element <- function(.data) { .default = outline_el ), outline_el = stringr::str_remove(outline_el, "[-\\=]{3,}") |> stringr::str_trim(), # remove trailing bars - is_subtitle = (is_tab_or_plot_title | is_doc_title) & grepl("subt", content, fixed = TRUE), + is_subtitle = (is_tab_or_plot_title | is_doc_title) & ( + grepl("subt", content, fixed = TRUE) | + tag %in% c("family", "concept")) ) + if (anyNA(x$outline_el)) { + zz <<- x |> dplyr::filter(is.na(outline_el)) + indices <- which(is.na(x$outline_el)) + all_na <- x |> + dplyr::select(!dplyr::where(\(x) !is.logical(x) & all(is.na(x)))) |> + dplyr::slice(dplyr::all_of(indices)) |> + dplyr::select(dplyr::where(\(x) all(is.na(x)))) |> + names() + all_true_or_single_value <- x |> + dplyr::slice(dplyr::all_of(indices)) |> + dplyr::select(dplyr::where(\(x) dplyr::n_distinct(x) == 1)) |> + dplyr::select(!dplyr::where(\(x) all(is.na(x)))) |> + dplyr::select(!dplyr::where(\(x) is.logical(x) & !suppressWarnings(any(x, na.rm = FALSE)))) |> + names() + if (length(all_na) > 0) { + msg <- c("The following places have all NAs {.var {all_na}}") + } else { + msg <- NULL + } + if (length(all_true_or_single_value) > 0) { + msg <- c(msg, "Likely problems in creating or displaying {.var {all_true_or_single_value}}.") + } + cli::cli_abort(c( + "Internal error, outline elements can't be NA. Please review.", msg, + paste0("{.file ", zz$file, ":", zz$line, "}"), + "Criteria are created in {.fn define_outline_criteria} and {.fn define_outline_criteria_roxy}. + `outline_el` is defined in {.fn display_outline_element}. Investigate `zz` for debugging." + )) + } + y <- dplyr::mutate( x, has_title_el = @@ -703,6 +786,7 @@ display_outline_element <- function(.data) { x } if (!all(is.na(y$title_el))) { + # browser() y <- dplyr::mutate( y, title_el = na_if0(title_el[!is.na(title_el)], "title"), @@ -713,11 +797,54 @@ display_outline_element <- function(.data) { y } +# With files with detected object captions, like fig.cap, title, tab.cap, tbl.cap. +extract_object_captions <- function(file) { + rlang::check_installed("lightparser", "to parse qmd files add chunk caption to outline.") + # we want fig-cap, tbl-cap and title + caps <- NULL + unique_file <- unique(file) + # ThinkR-open/lightparser#8 + for (i in seq_along(unique_file)) { + # FIXME find a way to be as consistent as lightparser, but faster. + dat <- tryCatch( + lightparser::split_to_tbl(unique_file[i]), + error = function(e) { + # workaround ThinkR-open/lightparser#11 + tmp <- withr::local_tempfile( + lines = c( + "---", + "title: dummy", + "---", + readLines(unique_file[i], warn = FALSE) + ) + ) + lightparser::split_to_tbl(tmp) + } + ) + dat <- dplyr::filter(dat, type == "block")$params + # Remove NA.. + dat <- purrr::discard(dat,\(x) isTRUE(is.na(x))) + # tidyverse/purrr#1081 + if (length(dat) > 0) { + # We use `format()` in case a variable is used to name the caption. + tryCatch(caps <- c(caps, dat |> purrr::map_chr(\(x) format(x[["fig-cap"]] %||% x[["tbl-cap"]] %||% x[["title"]] %||% x[["fig.cap"]] %||% x[["tbl.cap"]] %||% x[["tab.cap"]] %||% x[["cap"]] %||% "USELESS THING"))), error = function(e) { + cli::cli_abort("Error in {.file {unique_file[i]}}", parent = e) + }) + } + } + # used as a default to make sure purrr doesn't complain + caps <- caps[caps != "USELESS THING"] + if (length(caps) != length(file)) { + cli::cli_abort("error! :(, caps = {length(caps)}, file = {length(file)} in file {.file {unique_file}}") + } + caps |> stringr::str_squish() +} + define_important_element <- function(.data) { dplyr::mutate( .data, importance = dplyr::case_when( - is_second_level_heading_or_more | is_chunk_cap | is_cli_info | is_todo_fixme | is_subtitle | is_test_name ~ "not_important", + is_second_level_heading_or_more | is_obj_caption | is_cli_info | is_todo_fixme | is_subtitle | is_test_name ~ "not_important", .default = "important" ) ) diff --git a/R/proj-list.R b/R/proj-list.R index 1ee5303..5991f98 100644 --- a/R/proj-list.R +++ b/R/proj-list.R @@ -50,6 +50,11 @@ proj_switch <- function(proj = NA, new_session = TRUE) { proj_file <- function(file = NULL, path = active_rs_proj(), pattern = NULL) { rlang::check_required(file) check_proj(path, allow_null = TRUE) + + # avoid indexing roxy comments. + withr::local_options( + "reuseme.roxy_parse" = FALSE + ) # search will only be conducted with pattern if (is.null(pattern) && is.null(file)) { cli::cli_abort( diff --git a/README.Rmd b/README.Rmd index 6fec22d..86dd54f 100644 --- a/README.Rmd +++ b/README.Rmd @@ -57,7 +57,7 @@ To take advantage of reuseme, it is highly recommended to set the following opti options(reuseme.reposdir = c("~/rrr", "any-other-directories-that-contain-rstudio-projects")) ``` -This will enable functions like `proj_switch()`, `proj_list()`, `use_todo()` to be optimized. +This will enable functions like `proj_switch()`, `proj_list()`, `reuseme::use_todo()` to be optimized. ## Example diff --git a/README.md b/README.md index 1dcc7b8..cd975b7 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ options(reuseme.reposdir = c("~/rrr", "any-other-directories-that-contain-rstudi ``` This will enable functions like `proj_switch()`, `proj_list()`, -`use_todo()` to be optimized. +`reuseme::use_todo()` to be optimized. ## Example @@ -166,7 +166,7 @@ bench::mark( #> # A tibble: 1 × 6 #> expression min median `itr/sec` mem_alloc `gc/sec` #> -#> 1 outline <- proj_outline() 656ms 656ms 1.52 22.4MB 3.05 +#> 1 outline <- proj_outline() 5.61s 5.61s 0.178 89.6MB 1.25 ```
@@ -178,18 +178,29 @@ Example outline ``` r outline #> -#> ── `inst/example-file/outline-script.R` Example for `file_outline()` -#> `i` Load packages -#> `i` Wrangle + visualize data -#> `i` A great title -#> `i` TODO improve this Viz! +#> ── `R/browse-pkg.R` Browse pkgdown site if it exists [browse_pkg()] +#> +#> ── `R/case-if-any.R` case-when, but checks for all matches, returns a character [case_if_any()] #> #> ── `R/dplyr-plus.R` dplyr extra +#> `i` Count observations by group and compute percentage [count_pct()] +#> `i` dplyr extensions +#> `i` Subset rows using their positions [slice_min_max()] +#> `i` dplyr extensions +#> `i` Explore all rows in a random group [slice_group_sample()] +#> `i` family dplyr extensions #> `i` FIXME Doesn't work, problem with symbols here +#> `i` Keep rows that match one of the conditions [filter_if_any()] +#> `i` Elegant wrapper around filter and pull [extract_cell_value()] #> `i` TODO use `check_length()` when implemented. r-lib/rlang#1618 () #> `i` summarise with total +#> `i` Compute a summary for one group with the total included. [summarise_with_total()] +#> `i` Transform to NA any of the condition [na_if2()] #> #> ── `R/eda-identity.R` dplyr/base identity helpers -------------------- +#> `i` Helpers that return the same value [eda-identity] +#> `i` Use cases / advantages +#> `i` Caution #> `i` base identity functions #> `i` dplyr identity functions with small tweaks #> `i` dplyr identity without tweaks @@ -208,22 +219,39 @@ outline #> `i` Scalars #> `i` Vectors #> -#> ── `R/open.R` +#> ── `R/named.R` Helpers that can return a named vector [named-base] +#> +#> ── `R/open.R` Open a Document in RStudio [open_rs_doc()] #> `i` FIXME why is this code like this? +#> `i` Copy the active document to the same location [active_rs_doc_copy()] +#> `i` document manipulation helpers +#> `i` Delete the active RStudio document safely [active_rs_doc_delete()] +#> `i` document manipulation helpers #> `i` TODO structure and summarise information. #> `i` FIXME (upstream) the color div doesn't go all the way r-lib/cli#694 () +#> `i` Open Files Pane at current document location [active_rs_doc_nav()] #> -#> ── `R/outdated-pkgs.R` +#> ── `R/outdated-pkgs.R` Looks for outdated packages [outdated_pkgs()] #> `i` TODO figure out pad :) #> #> ── `R/outline-criteria.R` #> `i` Add variable to outline data frame #> `i` TODO extract title in roxy comments (@title too.L) -#> `i` TODO strip is_cli_info in Package? only valid for EDA -#> `i` FIXME try to detect all the chunk caption, but would have to figure out the end of it maybe lightparser. -#> `i` it is 'R/outline.R' +#> `i` TODO strip is_cli_info in Package? only valid for EDA (currently not showcased..) +#> `i` TODO long enough to be meanignful? +#> `i` TODO merge with define_outline_criteria +#> `i` it is 'R/outline.R' or 'R/outline-roxy.R' +#> +#> ── `R/outline-roxy.R` +#> `i` TODO when stable delete +#> `i` TODO Delete when stable debugging +#> `i` TODO Delete when stable for debugging +#> `i` TODO exclude S3 methods +#> `i` FIXME escape markup see next line +#> `i` helper for interactive checking #> #> ── `R/outline.R` `proj_outline()` +#> `i` Print interactive outline of file sections [outline] #> `i` `file_outline()` #> `i` File outline #> `i` TODO expand this to apply to most generated files @@ -232,12 +260,26 @@ outline #> `i` Step: tweak outline look as they show #> `i` TODO reanable cli info #> `i` TODO Improve performance with vctrs tidyverse/dplyr#6806 () +#> `i` FIXME find a way to be as consistent as lightparser, but faster. #> -#> ── `R/proj-list.R` +#> ── `R/proj-list.R` Opens a RStudio project in a new session [proj_switch()] +#> `i` project management helpers #> `i` TODO maybe add a max? +#> `i` Access the file outline within other project [proj_file()] +#> `i` project management helpers #> `i` TODO improve on this message +#> `i` Specify `proj` in functions [proj_list()] +#> `i` project management helpers +#> +#> ── `R/proj-reuseme.R` Interact with different RStudio projects [proj-reuseme] +#> `i` Setup +#> `i` Capabilities. +#> `i` project management helpers #> -#> ── `R/rename.R` +#> ── `R/quarto-help.R` Show links to Quarto documentation of interest [quarto_help()] +#> +#> ── `R/rename.R` Rename an output or a data file and watch for references [rename_files2()] +#> `i` Use case #> `i` After here, we start doing some renaming real situations #> `i` TODO verify if path should be normalized. #> `i` Helpers @@ -247,7 +289,9 @@ outline #> `i` FIXME maybe not fail while testing #> `i` TODO Check that old file is more recent #> -#> ── `R/todo.R` +#> ── `R/screenshot.R` Save the current image in clipboard to png in your active directory [screenshot()] +#> +#> ── `R/todo.R` Add a TODO list by project to a TODO.R file in the base directory [use_todo()] #> `i` TODO think about maybe using todo = clipr::read_clip() #> `i` TODO nice to have, but would need to extract duplicates #> `i` Helpers @@ -257,18 +301,52 @@ outline #> #> ── `R/utils.R` OS utils #> +#> ── `TODO.R` +#> `i` TODO screenshot make the behaviour different when vignettes vs articles: vignettes should place it in man/figures, while articles could put it in vignettes/articles file. +#> `i` TODO screenshot RStudio addin to insert the code directly in the qmd doc. No longer needed with RStudio 2023.12 +#> `i` TODO use_family() to edit .R file to add @family data frames tags to roxygen +#> `i` TODO mutate_identity redundant if the focus pillar PR was merged. r-lib/pillar#585 () +#> `i` TODO rename if many matches, separate those with the exact path. +#> `i` TODO outline make ggtitle work +#> `i` TODO outline show extra msg only for some, but in file outline, not in proj? +#> `i` TODO outline detect help calls and apply markup. `?fs::file_show` disregard finishing `.` (not followed by dot) +#> `i` TODO escape_markup doesn't work with complex operation {x^2} for example. Maybe if detecting something complex, use cli_escape function. escape-complex-markyp branch created to try to address this. +#> `i` TODO outline avoid evaluating in current env. +#> `i` TODO wrap regexps in functions +#> `i` TODO outline remove examples from outline. Sometimes commented code is caught. +#> `i` TODO outline roxygen comments processing should be left to `roxygen2::parse_file()` +#> `i` TODO outline show key like `pak::pkg_deps_tree()` does. +#> `i` TODO outline remove ggtext markup from plot title. +#> `i` FIXME outline comments are now interpreted as section +#> `i` TODO outline todos in qmd file inside html comment +#> `i` TODO reframe more than one issue. nw drive +#> `i` TODO delete generated files +#> `i` TODO [proj_file] to accesss data (return the path in this case?) +#> `i` TODO [check_referenced_files] doesn't check for 'R/file.R' +#> `i` TODO browse_pkg should open by default if no vignettes are found, because there is not much to do in the R-session. +#> `i` TODO exclude _files from `proj_list()` +#> `i` TODO outline Show function call if exported + not internal + bonus if has family tag! rstudio/rstudio#14766 () +#> `i` TODO title of file could be function title if it is first element [proj_outline] +#> `i` TODO rename_files should be less noisy about project name file +#> `i` TODO add_to_tricks(). when detecting TRICK like complete todo, but not remove line. requires a scheme. moves the item to tricks.md at the correct place. (copy to clipboard is probably enough) +#> `i` TODO outline just create an `exclude` argument that will take an option? (exclude can be files or expressionsm, or elements.) +#> `i` TODO outline remove snaps from outline and add a link in the test file instead? +#> `i` TODO outline family should be displayed differently.. +#> `i` TODO outline find a way to make print bookmarks.. +#> `i` TODO outline escape some content in headings see 'tests/testthat/_outline/quarto-caps.md' for examples. +#> +#> ── `inst/example-file/outline-script.R` Example for `file_outline()` +#> `i` Load packages +#> `i` Wrangle + visualize data +#> `i` A great title +#> `i` TODO improve this Viz! +#> +#> ── `playground/outline-tree.R` +#> `i` TODOs (they don't affect heirarchy) +#> #> ── `tests/testthat/_outline/knitr-notebook.R` Crop Analysis Q3 2013 #> `i` A great section #> -#> ── `tests/testthat/_outline/my-analysis.md` My doc title -#> `i` A section -#> `i` Dashboard card -#> `i` A code section -#> `i` A subsection -#> `i` A section2 -#> `i` A long ggplot2 title -#> `i` A code section -#> #> ── `tests/testthat/_outline/my-analysis.R` Analyse my {streets} #> `i` Read my streets () data #> `i` data wrangling @@ -277,6 +355,65 @@ outline #> `i` 'R/my-file.R' #> `i` Section title #> +#> ── `tests/testthat/_outline/my-analysis.md` My doc title +#> `i` A section +#> `i` Dashboard card +#> `i` A subsection +#> `i` A section2 +#> `i` A long ggplot2 title +#> `i` A code section +#> `i` A long ggplot2 title with more details2 +#> `i` A long ggplot2 title with more details3. +#> +#> ── `tests/testthat/_outline/quarto-caps.md` title +#> `i` A long ggplot2 title with more details +#> `i` Heading +#> `i` A long ggplot2 title with more details +#> `i` Heading2\_done +#> `i` Dashboard link +#> `i` Dashboard link +#> +#> ── `tests/testthat/_outline/roxy-cli.R` outline +#> `i` Like [base::grep()] but [grepl()] for ANSI strings [f2()] +#> +#> ── `tests/testthat/_outline/roxy-general.R` +#> `i` Use 'tests/testthat/_outline/roxy-general2.R' for output testing +#> `i` Complete block for exported function with headings +#> `i` A title to be included [f_to_be_index_in_outline()] +#> `i` A second-level heading in description to be included? +#> `i` A detail first level-heading to be included +#> `i` A detail second-level heading to be included +#> `i` `First code` to be included +#> `i` a family to include +#> `i` block not to index +#> `i` Topic to index +#> `i` A title to be included [topic-name-to-include] +#> `i` A second-level heading in description to be included? +#> `i` A detail first level-heading to be included +#> `i` A detail second-level heading to be included +#> `i` First to be included +#> `i` a family to include +#> `i` Opens a RStudio project in a new session +#> `i` second-level heading in desc +#> `i` Details + 2nd level heading +#> `i` second heading +#> `i` data to index +#> `i` My data [dataset] +#> +#> ── `tests/testthat/_outline/roxy-general2.R` Test for roxygen parsing for no error +#> `i` Use 'tests/testthat/_outline/roxy-general.R' for output testing +#> `i` Title with `_things` [f_to_be_index_in_outline()] +#> `i` Section +#> `i` a family to include +#> `i` An S3 method not to be include [f_not_to_index.xml()] +#> `i` section AA REQUIRED ELEMENT +#> +#> ── `tests/testthat/_outline/roxy-section.R` multiple tags + name parsing issue +#> `i` A title to be included [xxx] +#> `i` a section +#> `i` another section +#> `i` another sectio2n +#> #> ── `tests/testthat/_outline/title.md` The title is the only outline element #> #> ── `tests/testthat/_outline/titles.md` The title is the only outline element @@ -286,6 +423,19 @@ outline #> `i` Last title #> `i` `function_name()` title #> +#> ── `tests/testthat/_outline/tree.qmd` Test +#> `i` Quarto +#> `i` Running Code +#> `i` TODO fix this in the code +#> `i` A sub header +#> `i` TODO here's a todo in the text +#> `i` Back to header 1 +#> `i` Dont skip me +#> `i` header 5 +#> `i` TODO testing section +#> `i` Another sub header +#> `i` TODO section test +#> #> ── `tests/testthat/_snaps/browse-pkg.md` #> `i` browse_pkg() works #> @@ -313,12 +463,16 @@ outline #> ── `tests/testthat/_snaps/outline-criteria.md` #> `i` No outline criteria are untested #> +#> ── `tests/testthat/_snaps/outline-roxy.md` +#> `i` cli escaping goes well in roxy comments +#> #> ── `tests/testthat/_snaps/outline.md` #> `i` file_outline() works #> `i` alpha arguments works #> `i` file_outline() is a data frame #> `i` pattern works as expected #> `i` file_outline() detects correctly knitr notebooks +#> `i` file_outline() works well with figure captions #> #> ── `tests/testthat/_snaps/proj-list.md` #> `i` proj_file() works @@ -376,6 +530,11 @@ outline #> ── `tests/testthat/test-outline-criteria.R` Test individual outline elements #> `i` No outline criteria are untested #> +#> ── `tests/testthat/test-outline-roxy.R` +#> `i` roxy tags don't error +#> `i` multiple roxy tags don't error. +#> `i` cli escaping goes well in roxy comments +#> #> ── `tests/testthat/test-outline.R` #> `i` file_outline() is a data frame #> `i` TODO change tests for data frame size when stable (efficiency). As still debugging, better to keep all snapshots. diff --git a/TODO.R b/TODO.R index 3131e88..3ae6bb0 100644 --- a/TODO.R +++ b/TODO.R @@ -6,7 +6,6 @@ # TODO [outline] make ggtitle work # TODO [outline] show extra msg only for some, but in file outline, not in proj? # TODO [outline] detect help calls and apply markup. `?fs::file_show` disregard finishing `.` (not followed by dot) -# TODO [outline] renable cli info. # TODO escape_markup doesn't work with complex operation {{x^2}} for example. Maybe if detecting something complex, use cli_escape function. escape-complex-markyp branch created to try to address this. # TODO [outline] avoid evaluating in current env. # TODO wrap regexps in functions @@ -14,7 +13,6 @@ # TODO [outline] remove examples from outline. Sometimes commented code is caught. # TODO [outline] roxygen comments processing should be left to {.fn roxygen2::parse_file} # TODO [outline] show key like {.fn pak::pkg_deps_tree} does. -# TODO [outline] roxygen function title # TODO [outline] remove ggtext markup from plot title. # FIXME [outline] comments are now interpreted as section # TODO [outline] todos in qmd file inside html comment @@ -23,8 +21,15 @@ # TODO [check_referenced_files] doesn't check for {.file R/file.R} # TODO browse_pkg should open by default if no vignettes are found, because there is not much to do in the R-session. # TODO exclude _files from `proj_list()` +# TODO [outline] Show function call if exported + not internal + bonus if has family tag! rstudio/rstudio#14766 +# TODO title of file could be function title if it is first element [proj_outline] # TODO rename_files should be less noisy about project name file # TODO add_to_tricks(). when detecting TRICK like complete todo, but not remove line. requires a scheme. moves the item to tricks.md at the correct place. (copy to clipboard is probably enough) +# TODO [outline] just create an `exclude` argument that will take an option? (exclude can be files or expressionsm, or elements.) +# TODO [outline] remove snaps from outline and add a link in the test file instead? +# TODO [outline] family should be displayed differently.. +# TODO [outline] find a way to make print bookmarks.. +# TODO [outline] escape some content in headings see {.file tests/testthat/_outline/quarto-caps.md} for examples. # TODO use vapply() instead of purrr::map # TODO rename_files() should know about .covrignore too # TODO withr::local_dir for proj_outline. diff --git a/man/outline.Rd b/man/outline.Rd index 04fae7a..6b7d97e 100644 --- a/man/outline.Rd +++ b/man/outline.Rd @@ -11,13 +11,16 @@ file_outline( path = active_rs_doc(), pattern = NULL, alpha = FALSE, - print_todo = TRUE, - recent_only = FALSE + exclude_todos = FALSE, + recent_only = FALSE, + print_todo = deprecated() ) proj_outline( path = active_rs_proj(), pattern = NULL, + exclude_tests = FALSE, + exclude_todos = FALSE, dir_tree = FALSE, alpha = FALSE, recent_only = FALSE @@ -26,6 +29,8 @@ proj_outline( dir_outline( path = ".", pattern = NULL, + exclude_tests = FALSE, + exclude_todos = FALSE, dir_tree = FALSE, alpha = FALSE, recent_only = FALSE, @@ -38,15 +43,19 @@ Defaults to the \link[=active_rs_doc]{active file}, project or directory.} \item{pattern}{A string or regex to search for in the outline. If specified, will search only for elements matching this regular expression. -The print method will show the document title for context. Previously \code{regex_outline}} +The print method will show the document title for context. Previously \code{regex_outline}.} \item{alpha}{Whether to show in alphabetical order} -\item{print_todo}{Should include TODOs in the file outline? If \code{FALSE}, will +\item{exclude_todos}{Should include TODOs in the file outline? If \code{FALSE}, will print a less verbose output with sections.} \item{recent_only}{Show outline for recent files} +\item{print_todo}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}. Use \code{exclude_todos} instead.} + +\item{exclude_tests}{Should tests be displayed? (Not sure if this argument will stay, Will have to think)} + \item{dir_tree}{If \code{TRUE}, will print the \code{\link[fs:dir_tree]{fs::dir_tree()}} or non-R files in the directory} @@ -72,7 +81,7 @@ Outline elements include \item \code{TODO} items \item Parse cli hyperlinks \item Plot or table titles -\item FIgures caption in Quarto documents (limited support for multiline caption currently) +\item Figures caption in Quarto documents \item test names \item Indicator of recent modification \item Colored output for @@ -92,8 +101,7 @@ By default In \code{proj_outline()}, \code{path} accepts project names, see \code{\link[=proj_list]{proj_list()}} for how to set up reuseme to regognize your projects' locations. -The parser is very opinionated and is not very robust as it is based on regexps. -For a better file parser, explore other options, like \href{https://thinkr-open.github.io/lightparser/}{lightparser} for Quarto, \code{{roxygen2}} +The parser is opinionated and based on lightparser, roxygen2 and regexps. Will show TODO items and will offer a link to \link[=complete_todo]{mark them as complete}. @@ -106,7 +114,7 @@ file <- fs::path_package("reuseme", "example-file", "outline-script.R") file_outline(file) # Remove todo items -file_outline(file, print_todo = FALSE, alpha = TRUE) +file_outline(file, exclude_todos = TRUE, alpha = TRUE) # interact with data frame file_outline(file) |> dplyr::as_tibble() diff --git a/playground/outline-tree.R b/playground/outline-tree.R new file mode 100644 index 0000000..0b01d85 --- /dev/null +++ b/playground/outline-tree.R @@ -0,0 +1,111 @@ +# From @violetcereza https://github.com/olivroy/reuseme/issues/28#issuecomment-2128290268 +outline_data <- proj_outline() |> + + # Convert the many is_ columns into mutually exclusive "outline row types" + tidyr::pivot_longer( + names_to = "type", names_prefix = "is_", c( + dplyr::starts_with("is_"), -is_md, -is_second_level_heading_or_more + ) + ) %>% + # # Double check that types are mututally exclusive + # filter(all(value == F), .by = c(file_short, title, line_id)) + # filter(sum(value) > 1, .by = c(file_short, title, line_id)) + dplyr::filter(value) %>% + # We drop these because they don't serve to add much context to TODOs (they don't affect heirarchy) + dplyr::filter(type != "tab_or_plot_title") %>% + + # Some useful definitions! + dplyr::mutate( + title = dplyr::coalesce(outline_el, title_el), + file_short = fs::path_file(file), + n_leading_hash = type %>% dplyr::case_match( + c("todo_fixme", "tab_or_plot_title") ~ NA, + .default = n_leading_hash + ) + ) %>% + + # For each file, stick a item at the top of the outline + dplyr::group_by(file, file_short) %>% + group_modify(\(data, group) data %>% add_row( + .before = 0, + n_leading_hash = -1, + title = group$file_short, + type = "file" + )) %>% + + mutate( + # Processing how title displays based on type + print_title = dplyr::case_match( + type, + "todo_fixme" ~ link, + .default = link_rs_api + ) %>% dplyr::coalesce(title) %>% purrr::map_chr(cli::format_inline), + + # Assign TODO items (and other items missing n_leading_hash) + # to be indented under the last seen header level + indent = dplyr::coalesce(n_leading_hash, zoo::na.locf0(n_leading_hash+1)), + + # If there are any headers that skip an intermediate level, pick them up + skip_level = indent > dplyr::lag(indent)+1, + skip_level_should_be = ifelse(skip_level, dplyr::lag(indent)+1, NA), + skip_level_adjust = dplyr::case_when( + # All the items below on the outline should be adjusted backwards + skip_level ~ skip_level_should_be-indent, + # Unless we reach a point on the outline where we're back up in + # the hierachy, so stop adjusting. + indent <= zoo::na.locf0(skip_level_should_be) ~ 0 + ) %>% + # Carry the adjustments to later rows + zoo::na.locf0() %>% dplyr::coalesce(0), + + indent = indent + skip_level_adjust + ) |> + dplyr::ungroup() |> + dplyr::select(title, type, n_leading_hash, indent, print_title) + +dat_ready_to_tree <- outline_data %>% + dplyr::mutate( + # Give items IDs so titles do not have to be unique + item_id = as.character(dplyr::row_number()), + indent_wider = indent, + x = TRUE + ) %>% + + # We need these wide cumsum `header1` type fields to determine which items belong to which parents + tidyr::pivot_wider(names_from = indent_wider, values_from = x, values_fill = FALSE, names_prefix = "header") %>% + dplyr::mutate(dplyr::across(dplyr::starts_with("header"), cumsum)) %>% + + # For each row, pick the IDs of all direct children from the outline + purrr::pmap(function(...) with(list(..., childdata = .), tibble( + title, + print_title, + indent, + item_id, + type, + parent_level_id = get(stringr::str_c("header", indent)), + children_ids = childdata %>% + dplyr::rename(childindent = indent) %>% + dplyr::filter( + childindent == indent+1, + cumsum(childindent == indent) == parent_level_id + ) %>% + dplyr::pull(item_id) %>% list() + ))) %>% + purrr::list_rbind() %>% + # View() + dplyr::select(item_id, children_ids, print_title) + +dat_ready_to_tree |> + cli::tree() + +# browse-pkg +dat_ready_to_tree |> + cli::tree("5") + +dat_ready_to_tree |> + cli::tree("10") +dat_ready_to_tree |> + dplyr::filter(purrr::map_lgl(children_ids, \(x) length(x) > 0)) + +dat_ready_to_tree |> + cli::tree("13") diff --git a/playground/roxygen2-test.R b/playground/roxygen2-test.R deleted file mode 100644 index 71ffe07..0000000 --- a/playground/roxygen2-test.R +++ /dev/null @@ -1,29 +0,0 @@ -# pkgload::load_all() - -#' Extract roxygen tag -#' -#' Tell me what this does -#' -#' # Section to extract -#' -#' Well this is a section -#' -#' @md -#' @param file A file -#' @returns A named list with name = file:line, and element is the section title -extract_roxygen_tag_location <- function(file = c("R/proj-list.R"), tag) { - aa <- roxygen2::parse_file(file) - - pos <- purrr::map(aa, \(x) roxygen2::block_get_tags(x, tags = tag)) |> - purrr::list_flatten() - nam <- purrr::map(pos, \(x) paste0(x$file, ":", x$line)) - val <- purrr::map_chr(pos, "val") - rlang::set_names(val, nam) -} -titles_list <- purrr::map(fs::dir_ls("R"), \(x) extract_roxygen_tag_location(x, tag = "title")) |> - unlist() - -section_list <- purrr::map(fs::dir_ls("R"), \(x) extract_roxygen_tag_location(x, tag = "section")) |> - unlist() - -roxygen2::parse_file("R/rename-files.R") diff --git a/tests/testthat/_outline/my-analysis.md b/tests/testthat/_outline/my-analysis.md index db51fc3..c5f018e 100644 --- a/tests/testthat/_outline/my-analysis.md +++ b/tests/testthat/_outline/my-analysis.md @@ -12,9 +12,6 @@ title: My doc title ```{r} #| title: Dashboard card - -# A code section ---- - ``` ## A subsection @@ -31,3 +28,15 @@ Some text with TODO. # A code section ---- ``` + +```{r} +#| fig-cap: > +#| A long ggplot2 title +#| with more details2 +``` + +```{r} +#| fig.cap = " +#| A long ggplot2 title +#| with more details3." +``` diff --git a/tests/testthat/_outline/quarto-caps.md b/tests/testthat/_outline/quarto-caps.md new file mode 100644 index 0000000..a04e8c5 --- /dev/null +++ b/tests/testthat/_outline/quarto-caps.md @@ -0,0 +1,42 @@ +--- +title: title +--- + +```{r} +#| fig-cap: > +#| A long ggplot2 title +#| with more details +``` + +## Heading + +Remove the html and if possible, just print an emoji? maybe even check font-awesome. the emoji package could be used? + +```{r} +#| label: another-lab +#| tbl-cap: | +#| A long ggplot2 title +#| with more details +``` + +## Heading2\_done + +Remove the backslash + +```{r} +#| title: Dashboard link +``` + +```{r, fig.cap ="Dashboard link"} +caption +``` + +```{r, fig.alt ="the caption"} +caption +``` + +```{r} +#| label: another-lab +#| fig-alt: | +#| The caption and title are left justified, the legend is inside of the plot +``` diff --git a/tests/testthat/_outline/roxy-cli.R b/tests/testthat/_outline/roxy-cli.R new file mode 100644 index 0000000..1d97a26 --- /dev/null +++ b/tests/testthat/_outline/roxy-cli.R @@ -0,0 +1,17 @@ +# outline -------- +#' Like [base::grep()] but [grepl()] for ANSI strings +#' +#' kk +#' +#' @param x A title +#' +#' @section Escaping `{` and `}`: +#' +#' things +#' @export +f2 <- function(x) { + x**2 +} + + +# Keep this line last: content to test for new roxygen output should be put in roxy-general.R diff --git a/tests/testthat/_outline/roxy-general.R b/tests/testthat/_outline/roxy-general.R new file mode 100644 index 0000000..a9c5e98 --- /dev/null +++ b/tests/testthat/_outline/roxy-general.R @@ -0,0 +1,107 @@ +# This file is for roxygen comments parsing +## Use {.file tests/testthat/_outline/roxy-general2.R} for output testing ----------- + +## Complete block for exported function with headings -------------------------- +# Mix md and `@section` +#' A title to be included +#' +#' A description not to be included +#' +#' ## A second-level heading in description to be included? +#' +#' # A detail first level-heading to be included +#' +#' Content not to be included +#' +#' ## A detail second-level heading to be included +#' +#' Content not to be included2 +#' +#' @section `First code` to be included: +#' +#' Content not to be included +#' +#' @examples +#' # Commented code not included +#' +#' ggplot2::ggplot(mtcars) + +#' labs( +#' title = "A title not to be included" +#' ) +#' @export +#' @family a family to include +f_to_be_index_in_outline <- function() { + +} + +# block not to index ----------------------------------------------------------- +#' A title not to be included (internal function) +#' +#' An internal description not to include +#' +#' # Internal heading not to be included +#' +#' content +#' @keywords internal +#' @export +f_not_to_index <- function() { + +} +# Topic to index ----------------------------------- + +#' A title to be included +#' +#' A description not to be included +#' +#' ## A second-level heading in description to be included? +#' +#' # A detail first level-heading to be included +#' +#' Content not to be included +#' +#' ## A detail second-level heading to be included +#' +#' Content not to be included2 +#' +#' # First to be included +#' +#' Content not to be included +#' +#' @examples +#' # Commented code not included +#' +#' ggplot2::ggplot(mtcars) + +#' labs( +#' title = "A title not to be included" +#' ) +#' @name topic-name-to-include +#' @family a family to include +NULL + +#' Opens a RStudio project in a new session +#' @description +#' If not specified, will generate hyperlinks that call [usethis::proj_activate()]. +#' `proj_switch()` looks at `options(reuseme.reposdir)`. +#' +#' ## second-level heading in desc +#' +#' content +#' +#' # Details + 2nd level heading +#' content +#' +#' ## second heading +#' +#' Content +#' +NULL +# data to index ---------------------------------------------------------------- + +# I think I'd want the outline to show as "outline": title (but Ctrl + . does a good job for this) (maybe document this) + +#' My data +#' +#' Another dataset +"dataset" + +# Keep this line last: content to test for edge cases should be put in {.file tests/testthat/_outline/roxy-general2.R} diff --git a/tests/testthat/_outline/roxy-general2.R b/tests/testthat/_outline/roxy-general2.R new file mode 100644 index 0000000..961fb96 --- /dev/null +++ b/tests/testthat/_outline/roxy-general2.R @@ -0,0 +1,59 @@ +# Test for roxygen parsing for no error ---------------------------------------- +## Use {.file tests/testthat/_outline/roxy-general.R} for output testing ----------- +# Dump of other things to test for expect_no_error (not necessary to verify) +# Mostly cases inspired by testing in the wild. + +#' Title with `_things` +#' +#' @description +#' ## Section +#' +#' +#' ```r +#' # Commented code not include +#' title = "TITLE NOT INCLUDED" +#' ``` +#' +#' @examples +#' # Commented code not included +#' +#' ggplot2::ggplot(mtcars) + +#' labs( +#' title = "A title not to be included" +#' ) +#' @export +#' @family a family to include +#' if { +#' } +f_to_be_index_in_outline <- function() { + +} + +#' An S3 method not to be include +#' +#' content +#' @export +f_not_to_index.xml <- function() { + +} + +#' A +#' +#' Very short title. +#' @export +f_not_to_index <- function() { + +} +NULL + +#' A t +#' +#' desc +#' +#' @section section AA REQUIRED ELEMENT: +#' +#' * DO this +#' +NULL + +# Keep this line last: content to test for new roxygen output should be put in roxy-general.R diff --git a/tests/testthat/_outline/roxy-section.R b/tests/testthat/_outline/roxy-section.R new file mode 100644 index 0000000..551ee2f --- /dev/null +++ b/tests/testthat/_outline/roxy-section.R @@ -0,0 +1,23 @@ +## multiple tags + name parsing issue -------------------------- +# Mix md and `@section` +#' A title to be included +#' +#' A description not to be included +#' +#' @section a section: +#' +#' +#' @section another section: +#' +#' A second-level heading in description to be included? +#' +#' # A detail first level-heading to be included +#' @name xxx +NULL +#' @section another sectio2n: +#' +#' A second-level heading in description to be included? +#' +#' # A detail first level-heading to be included +#' @name yyy +NULL diff --git a/tests/testthat/_outline/tree.qmd b/tests/testthat/_outline/tree.qmd new file mode 100644 index 0000000..b1bb6f3 --- /dev/null +++ b/tests/testthat/_outline/tree.qmd @@ -0,0 +1,45 @@ +--- +title: "Test" +format: html +editor: source +--- + +## Quarto + +Quarto enables you to weave together content and executable code into a finished document. +To learn more about Quarto see . + +## Running Code + +When you click the **Render** button a document will be generated that includes both content and the output of embedded code. +You can embed code like this: + +```{r} +1 + 1 +# TODO: fix this in the code +``` + +You can add options to executable code like this + +### A sub header + +```{r} +#| echo: false +2 * 2 +``` + +The `echo: false` option disables the printing of code (only output is displayed). + +TODO: here's a todo in the text + +# Back to header 1 + +## Dont skip me + +##### header 5 + +TODO: testing section + +## Another sub header + +TODO: section test diff --git a/tests/testthat/_snaps/outline-roxy.md b/tests/testthat/_snaps/outline-roxy.md new file mode 100644 index 0000000..cdf86df --- /dev/null +++ b/tests/testthat/_snaps/outline-roxy.md @@ -0,0 +1,10 @@ +# cli escaping goes well in roxy comments + + Code + file_outline(path = file_to_map) + Message + + -- `roxy-cli.R` outline + Output + `i` Like [base::grep()] but [grepl()] for ANSI strings [f2()] + diff --git a/tests/testthat/_snaps/outline.md b/tests/testthat/_snaps/outline.md index f9c442c..b46b1e9 100644 --- a/tests/testthat/_snaps/outline.md +++ b/tests/testthat/_snaps/outline.md @@ -7,11 +7,12 @@ -- `my-analysis.md` My doc title Output `i` A section - `i` A code section `i` A subsection `i` A section2 `i` A long ggplot2 title `i` A code section + `i` A long ggplot2 title with more details2 + `i` A long ggplot2 title with more details3. `i` Dashboard card Message @@ -96,3 +97,18 @@ Output `i` A great section +# file_outline() works well with figure captions + + Code + file_outline(path = test_path("_outline", "quarto-caps.md")) + Message + + -- `quarto-caps.md` title + Output + `i` A long ggplot2 title with more details + `i` Heading + `i` A long ggplot2 title with more details + `i` Heading2\_done + `i` Dashboard link + `i` Dashboard link + diff --git a/tests/testthat/test-outline-criteria.R b/tests/testthat/test-outline-criteria.R index 5828236..3e5efb1 100644 --- a/tests/testthat/test-outline-criteria.R +++ b/tests/testthat/test-outline-criteria.R @@ -54,8 +54,8 @@ test_that("o_is_section_title() works", { expect_true(o_is_section_title("# Analysis of this")) expect_true(o_is_section_title(" # section 1 ----")) expect_false(o_is_section_title("# TidyTuesday")) - # not considering roxygen sections anymore. - expect_false(o_is_section_title("#' # A very interesting section")) + expect_false(o_is_section_title("@section: Function ID:", roxy_section = TRUE)) + expect_false(o_is_section_title("#' @section Function ID:", roxy_section = TRUE)) }) test_that("o_is_cli_info() works", { diff --git a/tests/testthat/test-outline-roxy.R b/tests/testthat/test-outline-roxy.R new file mode 100644 index 0000000..db4d690 --- /dev/null +++ b/tests/testthat/test-outline-roxy.R @@ -0,0 +1,65 @@ +test_that("roxy tags are parsed properly + object names are correct", { + skip_if_not_installed("roxygen2") + skip_if_not_installed("tidyr") + file_to_map <- testthat::test_path("_outline", "roxy-general.R") + names(file_to_map) <- file_to_map + example_parsed <- purrr::map(file_to_map, \(x) roxygen2::parse_file(x, env = NULL)) + + example_parsed |> + extract_roxygen_tag_location("details") |> + expect_no_error() + + expect_no_error(res <- join_roxy_fun(example_parsed)) + + expect_s3_class(res, "tbl_df") + # verify if topic name is well done. + res_order <- dplyr::arrange(res, line) + expect_setequal( + res$topic, + c("f_to_be_index_in_outline()", "topic-name-to-include", NA_character_, "dataset") + ) + # strip code from roxygen2 tag + expect_contains(res$content, "`First code` to be included") +}) + +test_that("roxy tags don't error", { + file_to_map <- testthat::test_path("_outline", "roxy-general2.R") + names(file_to_map) <- file_to_map + example_parsed <- purrr::map(file_to_map, \(x) roxygen2::parse_file(x, env = NULL)) + expect_no_error(join_roxy_fun(example_parsed)) +}) + +test_that("multiple roxy tags don't error.", { + file_to_map <- testthat::test_path("_outline", "roxy-section.R") + names(file_to_map) <- file_to_map + example_parsed <- purrr::map(file_to_map, \(x) roxygen2::parse_file(x, env = NULL)) + expect_no_error(join_roxy_fun(example_parsed)) +}) + +test_that("file_outline() works outside RStudio)", { + skip_on_cran() + local_mocked_bindings( + is_rstudio = function(...) FALSE + ) + expect_no_warning( + file_outline(path = testthat::test_path("_outline", "roxy-cli.R")) + ) + expect_equal( + nrow(file_outline(path = testthat::test_path("_outline", "roxy-cli.R"))), + nrow(file_outline(path = normalizePath(testthat::test_path("_outline", "roxy-cli.R")))) + ) +}) + +test_that("cli escaping goes well in roxy comments", { + rlang::local_interactive(FALSE) + # The fact that I need to do this is bizzare. + file_to_map <- testthat::test_path("_outline", "roxy-cli.R") + expect_snapshot( + file_outline(path = file_to_map), + transform = ~ sub(" `[^`]+` ", " `roxy-cli.R` ", .x) + + ) + names(file_to_map) <- file_to_map + example_parsed <- purrr::map(file_to_map, \(x) roxygen2::parse_file(x, env = NULL)) + expect_no_error(join_roxy_fun(example_parsed)) +}) diff --git a/tests/testthat/test-outline.R b/tests/testthat/test-outline.R index 2712784..c9b8bb4 100644 --- a/tests/testthat/test-outline.R +++ b/tests/testthat/test-outline.R @@ -1,4 +1,5 @@ test_that("file_outline() works", { + skip_if_not_installed("lightparser") my_test_files <- test_path("_outline", c("my-analysis.R", "my-analysis.md", "title.md", "titles.md")) rlang::local_interactive(TRUE) expect_snapshot( @@ -58,6 +59,7 @@ test_that("pattern works as expected", { }) test_that("file_outline() with only title doesn't error", { + # broken by change to before_and_after_empty expect_no_error({ file <- file_outline(test_path("_outline", "title.md")) }) @@ -78,6 +80,7 @@ test_that("file_outline() contains function calls", { }) test_that("dir_outline() works with no error", { + skip_if_not_installed("lightparser") expect_no_error(dir_outline(path = test_path("_outline"), pattern = ".+")) }) @@ -87,3 +90,11 @@ test_that("file_outline() detects correctly knitr notebooks", { transform = ~ sub(" `[^`]+` ", " `knitr-notebook.R` ", .x) ) }) + +test_that("file_outline() works well with figure captions", { + skip_if_not_installed("lightparser") + expect_snapshot( + file_outline(path = test_path("_outline", "quarto-caps.md")), + transform = ~ sub(" `[^`]+` ", " `quarto-caps.md` ", .x) + ) +})