From fc8472cd53a04499cb1e305b7d32bd6c8a339ff8 Mon Sep 17 00:00:00 2001 From: "Jennifer (Jenny) Bryan" Date: Mon, 4 Mar 2024 13:26:55 -0600 Subject: [PATCH] Use cli (#1956) * Init article on the conversion task * Build up the cli-based UI in one place * Introduce ui_cli_bullets() * author.R * Factor out the path-processing logic into ui_path_impl() * addin.R * ui_bullets() is better because shorter * badge.R * ui_abort() feels more consistent with everything else * Introduce ui_code_snippet() * block.R * Some testing improvements * Test ui_code_snippet() * Replace all calls to ui_code_block() * Typo * Update conversion article * browse.R * Eliminate usage of ui_warn() * ci.R * code_of_conduct.R * course.R * coverage.R * cpp11.R * Single quote inline .field if no color * create.R * data.R * description.R * directory.R * edit.R * git-default-branch.R * git.R * Test bulletize() and usethis_map_cli() * github-actions.R * .val seems like a better default for usethis * github-labels.R * github-pages.R * github.R + check_current_branch() * github_token.R * Remove hd_line() which is not used anywhere at this point * Notes * Modernize kv_line() + other changes to improve git_sitrep() * github_token.R * helpers.R * issue.R * license.R * lifecycle.R * logo.R * news.R * package.R * pkgdown.R * Provide default bullets in ui_abort() * pr.R * proj-desc.R * proj.R * rcpp.R * readme.R * release.R * rename-files.R * revdep.R * roxygen.R * rprofile.R * rstudio.R * sitrep.R * spelling.R * template.R * test.R * tibble.R * tidyverse.R * upkeep.R * use_github_file.R * use_import_fun.R * usethis-defunct.R * Rename to ui_legacy_bullet(); catch up on note-taking * utils-git.R * utils.R * version.R * vignette.R * write.R * utils-github.R * Add snapshot tests for remote GitHub configurations * Finish converting UI around GitHub remote configurations * Refactor flaky test * Better organization * Catch up on note-taking * Work on making the article actually render the way I want * Mark ui_*() functions as superseded * Mark ui_yeah() and ui_no() as superseded * Work on kv_line() * Use ui_yep() and ui_nah() everywhere * Re-align filenames re: legacy ui * OMG I don't want people to depend on usethis for ui functions * Switch to ui_nah() , for real * Make sure I don't call into the legacy file * Deal with stragglers that are not TODOs * Update principles.md * NEWS bullet * Yet another straggler * Deal with utils::menu() prep and other unusual stuff * Make test less sensitive to local conditions * Article needs asciicast * Increase width to forcibly prevent snapshot diff due to linebreaks I guess temp paths vary in length, even across separate runs on the same platform? * Organize the articles * Update README * Make the width even larger??? * Apply suggestions from code review Co-authored-by: olivroy <52606734+olivroy@users.noreply.github.com> * Update snapshot * How about cli.width????? * YOLO * `.run` fixups * These run afoul of the `.run` rules, so why bother * Better captures the intent --------- Co-authored-by: olivroy <52606734+olivroy@users.noreply.github.com> --- DESCRIPTION | 2 +- NAMESPACE | 3 + NEWS.md | 13 + R/addin.R | 6 +- R/author.R | 53 +- R/badge.R | 24 +- R/block.R | 26 +- R/browse.R | 39 +- R/ci.R | 4 +- R/code-of-conduct.R | 14 +- R/course.R | 76 +-- R/coverage.R | 11 +- R/cpp11.R | 4 +- R/create.R | 102 ++-- R/data-table.R | 8 +- R/data.R | 37 +- R/description.R | 12 +- R/directory.R | 10 +- R/edit.R | 28 +- R/git-default-branch.R | 200 ++++---- R/git.R | 148 +++--- R/github-actions.R | 16 +- R/github-labels.R | 45 +- R/github-pages.R | 13 +- R/github.R | 43 +- R/github_token.R | 139 +++--- R/helpers.R | 34 +- R/issue.R | 44 +- R/license.R | 3 +- R/lifecycle.R | 18 +- R/logo.R | 16 +- R/news.R | 4 +- R/package.R | 72 +-- R/pkgdown.R | 34 +- R/pr.R | 300 +++++++----- R/proj-desc.R | 9 +- R/proj.R | 34 +- R/rcpp.R | 17 +- R/readme.R | 10 +- R/release.R | 44 +- R/rename-files.R | 20 +- R/revdep.R | 4 +- R/roxygen.R | 47 +- R/rprofile.R | 28 +- R/rstudio.R | 6 +- R/sitrep.R | 76 ++- R/spelling.R | 2 +- R/template.R | 7 +- R/test.R | 19 +- R/tibble.R | 8 +- R/tidyverse.R | 44 +- R/{ui.R => ui-legacy.R} | 128 ++--- R/upkeep.R | 11 +- R/use_github_file.R | 8 +- R/use_import_from.R | 20 +- R/usethis-defunct.R | 28 +- R/utils-git.R | 155 +++--- R/utils-github.R | 430 ++++++++++++----- R/utils-ui.R | 260 +++++++++- R/utils.R | 10 +- R/version.R | 5 +- R/vignette.R | 8 +- R/write.R | 6 +- README.Rmd | 2 +- README.md | 66 +-- _pkgdown.yml | 21 +- man/git_vaccinate.Rd | 11 +- man/{ui.Rd => ui-legacy-functions.Rd} | 42 +- man/ui-questions.Rd | 16 +- man/ui_silence.Rd | 25 + man/use_import_from.Rd | 2 +- principles.md | 12 +- tests/testthat/_snaps/author.md | 24 +- tests/testthat/_snaps/badge.md | 6 +- tests/testthat/_snaps/course.md | 16 +- tests/testthat/_snaps/cpp11.md | 2 +- tests/testthat/_snaps/data-table.md | 14 + tests/testthat/_snaps/git-default-branch.md | 20 +- tests/testthat/_snaps/github-actions.md | 22 +- tests/testthat/_snaps/github.md | 4 +- tests/testthat/_snaps/helpers.md | 63 ++- tests/testthat/_snaps/lifecycle.md | 14 +- tests/testthat/_snaps/package.md | 35 +- tests/testthat/_snaps/pkgdown.md | 28 +- tests/testthat/_snaps/proj-desc.md | 6 +- tests/testthat/_snaps/proj.md | 12 +- tests/testthat/_snaps/rename-files.md | 2 +- tests/testthat/_snaps/roxygen.md | 16 + tests/testthat/_snaps/tibble.md | 6 +- tests/testthat/_snaps/tidyverse.md | 21 +- tests/testthat/_snaps/{ui.md => ui-legacy.md} | 12 +- tests/testthat/_snaps/use_import_from.md | 12 +- tests/testthat/_snaps/utils-github.md | 188 +++++++- tests/testthat/_snaps/utils-ui.md | 455 ++++++++++++++++++ tests/testthat/_snaps/utils.md | 24 + tests/testthat/_snaps/vignette.md | 8 +- tests/testthat/_snaps/write.md | 2 +- tests/testthat/helper-mocks.R | 4 +- tests/testthat/helper.R | 14 +- tests/testthat/test-data-table.R | 7 +- tests/testthat/test-helpers.R | 44 +- tests/testthat/test-lifecycle.R | 2 +- tests/testthat/test-package.R | 4 +- tests/testthat/test-pkgdown.R | 9 +- tests/testthat/test-roxygen.R | 4 +- .../testthat/{test-ui.R => test-ui-legacy.R} | 23 +- tests/testthat/test-upkeep.R | 10 +- tests/testthat/test-utils-github.R | 58 ++- tests/testthat/test-utils-ui.R | 231 +++++++++ tests/testthat/test-utils.R | 10 +- vignettes/articles/ui-cli-conversion.Rmd | 442 +++++++++++++++++ 111 files changed, 3631 insertions(+), 1485 deletions(-) rename R/{ui.R => ui-legacy.R} (75%) rename man/{ui.Rd => ui-legacy-functions.Rd} (68%) create mode 100644 man/ui_silence.Rd create mode 100644 tests/testthat/_snaps/roxygen.md rename tests/testthat/_snaps/{ui.md => ui-legacy.md} (65%) create mode 100644 tests/testthat/_snaps/utils-ui.md create mode 100644 tests/testthat/_snaps/utils.md rename tests/testthat/{test-ui.R => test-ui-legacy.R} (51%) create mode 100644 tests/testthat/test-utils-ui.R create mode 100644 vignettes/articles/ui-cli-conversion.Rmd diff --git a/DESCRIPTION b/DESCRIPTION index f64862fd1..5c101feb3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -53,7 +53,7 @@ Suggests: spelling (>= 1.2), styler (>= 1.2.0), testthat (>= 3.1.8) -Config/Needs/website: tidyverse/tidytemplate, xml2 +Config/Needs/website: r-lib/asciicast, tidyverse/tidytemplate, xml2 Config/testthat/edition: 3 Config/testthat/parallel: TRUE Config/testthat/start-first: github-actions, release diff --git a/NAMESPACE b/NAMESPACE index b6bcb9a93..3648d5db7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,9 @@ S3method(format,github_remote_config) S3method(print,github_remote_config) S3method(print,sitrep) +S3method(usethis_map_cli,"NULL") +S3method(usethis_map_cli,character) +S3method(usethis_map_cli,default) export(browse_circleci) export(browse_cran) export(browse_github) diff --git a/NEWS.md b/NEWS.md index 35cba70f6..d7f3a70a0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,18 @@ # usethis (development version) +* The `ui_*()` functions have been marked as + [superseded](https://lifecycle.r-lib.org/articles/stages.html#superseded). + External users of these functions are encouraged to use the + [cli package](https://cli.r-lib.org/) instead. + The cli package did not have the required functionality when the + `usethis::ui_*()` functions were first created, but it does now and it's the + superior option. + There is a cli vignette about how to make this transition: + `vignette("usethis-ui", package = "cli")`. + + usethis no longer uses the `ui_*()` functions internally, in favor of new + cli-based helpers that are not exported. + # usethis 2.2.3 * Patch release with changes to `.Rd` files requested by CRAN. diff --git a/R/addin.R b/R/addin.R index 1072d7115..6f4a5e905 100644 --- a/R/addin.R +++ b/R/addin.R @@ -15,13 +15,15 @@ use_addin <- function(addin = "new_addin", open = rlang::is_interactive()) { if (!file_exists(addin_dcf_path)) { create_directory(proj_path("inst", "rstudio")) file_create(addin_dcf_path) - ui_done("Creating {ui_path(addin_dcf_path)}") + ui_bullets(c("v" = "Creating {.path {pth(addin_dcf_path)}}")) } addin_info <- render_template("addins.dcf", data = list(addin = addin)) addin_info[length(addin_info) + 1] <- "" write_utf8(addin_dcf_path, addin_info, append = TRUE) - ui_done("Adding binding to {ui_code(addin)} to addins.dcf.") + ui_bullets(c( + "v" = "Adding binding to {.fun {addin}} to {.path addins.dcf}" + )) if (open) { edit_file(addin_dcf_path) diff --git a/R/author.R b/R/author.R index b31221dd5..7e396e4ac 100644 --- a/R/author.R +++ b/R/author.R @@ -60,13 +60,15 @@ use_author <- function(given = NULL, family = NULL, ..., role = "ctb") { author <- utils::person(given = given, family = family, role = role, ...) aut_fmt <- format(author, style = 'text') if (authors_at_r_already) { - ui_done(" - Adding to {ui_field('Authors@R')} in DESCRIPTION: - {aut_fmt}") + ui_bullets(c( + "v" = "Adding to {.field Authors@R} in DESCRIPTION:", + " " = "{aut_fmt}" + )) } else { - ui_done(" - Creating {ui_field('Authors@R')} field in DESCRIPTION and adding: - {aut_fmt}") + ui_bullets(c( + "v" = "Creating {.field Authors@R} field in DESCRIPTION and adding:", + " " = "{aut_fmt}" + )) } d$add_author(given = given, family = family, role = role, ...) @@ -84,17 +86,17 @@ challenge_legacy_author_fields <- function(d = proj_desc()) { return(invisible()) } - ui_oops(" - Found legacy {ui_field('Author')} and/or {ui_field('Maintainer')} field \\ - in DESCRIPTION. - usethis only supports modification of the {ui_field('Authors@R')} field.") - ui_info(" - We recommend one of these paths forward: - * Delete these fields and rebuild with {ui_code('use_author()')}. - * Convert to {ui_field('Authors@R')} with {ui_code('desc::desc_coerce_authors_at_r()')}, - then delete the legacy fields.") - if (ui_yeah("Do you want to cancel this operation and sort that out first?")) { - ui_stop("Cancelling.") + ui_bullets(c( + "x" = "Found legacy {.field Author} and/or {.field Maintainer} field in + DESCRIPTION.", + " " = "usethis only supports modification of the {.field Authors@R} field.", + "i" = "We recommend one of these paths forward:", + "_" = "Delete the legacy fields and rebuild with {.fun use_author}; or", + "_" = "Convert to {.field Authors@R} with + {.fun desc::desc_coerce_authors_at_r}, then delete the legacy fields." + )) + if (ui_yep("Do you want to cancel this operation and sort that out first?")) { + ui_abort("Cancelling.") } invisible() } @@ -108,10 +110,10 @@ check_author_is_novel <- function(given = NULL, family = NULL, d = proj_desc()) }) if (any(m)) { aut_name <- glue("{given %||% ''} {family %||% ''}") - usethis_abort(c( - "{.val {aut_name}} already appears in {.val Authors@R}.", - "Please make the desired change directly in DESCRIPTION or call the \\ - desc package directly." + ui_abort(c( + "x" = "{.val {aut_name}} already appears in {.field Authors@R}.", + " " = "Please make the desired change directly in DESCRIPTION or call the + {.pkg desc} package directly." )) } invisible() @@ -129,10 +131,11 @@ challenge_default_author <- function(d = proj_desc()) { ) if (any(m)) { - ui_info(" - {ui_field('Authors@R')} appears to include a placeholder author: - {format(default_author, style = 'text')}") - if(is_interactive() && ui_yeah("Would you like to remove it?")) { + ui_bullets(c( + "i" = "{.field Authors@R} appears to include a placeholder author:", + " " = "{format(default_author, style = 'text')}" + )) + if(is_interactive() && ui_yep("Would you like to remove it?")) { # TODO: Do I want to suppress this output? # Authors removed: First Last, NULL NULL. do.call(d$del_author, unclass(default_author)[[1]]) diff --git a/R/badge.R b/R/badge.R index b1317cf5b..cba764169 100644 --- a/R/badge.R +++ b/R/badge.R @@ -43,15 +43,15 @@ NULL use_badge <- function(badge_name, href, src) { path <- find_readme() if (is.null(path)) { - ui_oops(" - Can't find a README for the current project. - See {ui_code('usethis::use_readme_rmd()')} for help creating this file. - Badge link can only be printed to screen. - ") + ui_bullets(c( + "!" = "Can't find a README for the current project.", + "i" = "See {.fun usethis::use_readme_rmd} for help creating this file.", + "i" = "Badge link will only be printed to screen." + )) path <- "README" } changed <- block_append( - glue("{ui_field(badge_name)} badge"), + glue("{badge_name} badge"), glue("[![{badge_name}]({src})]({href})"), path = path, block_start = badge_start, @@ -59,7 +59,9 @@ use_badge <- function(badge_name, href, src) { ) if (changed && path_ext(path) == "Rmd") { - ui_todo("Re-knit {ui_path(path)} with {ui_code('devtools::build_readme()')}") + ui_bullets(c( + "_" = "Re-knit {.path {pth(path)}} with {.fun devtools::build_readme}." + )) } invisible(changed) } @@ -153,10 +155,10 @@ use_posit_cloud_badge <- function(url) { img <- "https://img.shields.io/badge/launch-posit%20cloud-447099?style=flat" use_badge("Launch Posit Cloud", url, img) } else { - usethis_abort(" - {.fun usethis::use_posit_cloud_badge} requires a link to an \\ - existing Posit Cloud project of the form \\ - {.val https://posit.cloud/content/} or \\ + ui_abort(" + {.fun usethis::use_posit_cloud_badge} requires a link to an + existing Posit Cloud project of the form + {.val https://posit.cloud/content/} or {.val https://posit.cloud/spaces//content/}.") } diff --git a/R/block.R b/R/block.R index df7987393..01d768a35 100644 --- a/R/block.R +++ b/R/block.R @@ -16,13 +16,14 @@ block_append <- function(desc, value, path, } if (is.null(block_lines)) { - ui_todo(" - Copy and paste the following lines into {ui_path(path)}:") - ui_code_block(c(block_prefix, block_start, value, block_end, block_suffix)) + ui_bullets(c( + "_" = "Copy and paste the following lines into {.path {pth(path)}}:" + )) + ui_code_snippet(c(block_prefix, block_start, value, block_end, block_suffix)) return(FALSE) } - ui_done("Adding {desc} to {ui_path(path)}") + ui_bullets(c("v" = "Adding {.val {desc}} to {.path {pth(path)}}.")) start <- block_lines[[1]] end <- block_lines[[2]] @@ -54,8 +55,10 @@ block_replace <- function(desc, value, path, } if (is.null(block_lines)) { - ui_todo("Copy and paste the following lines into {ui_value(path)}:") - ui_code_block(c(block_start, value, block_end)) + ui_bullets(c( + "_" = "Copy and paste the following lines into {.path {pth(path)}}:" + )) + ui_code_snippet(c(block_start, value, block_end)) return(invisible(FALSE)) } @@ -67,7 +70,7 @@ block_replace <- function(desc, value, path, return(invisible(FALSE)) } - ui_done("Replacing {desc} in {ui_path(path)}") + ui_bullets(c("v" = "Replacing {desc} in {.path {pth(path)}}.")) lines <- c( lines[seq2(1, start - 1L)], @@ -99,10 +102,11 @@ block_find <- function(lines, block_start = "# <<<", block_end = "# >>>") { } if (!(length(start) == 1 && length(end) == 1 && start < end)) { - ui_stop( - "Invalid block specification. - Must start with {ui_code(block_start)} and end with {ui_code(block_end)}" - ) + ui_abort(c( + "Invalid block specification.", + "Must start with {.code {block_start}} and end with + {.code {block_end}}." + )) } c(start + 1L, end - 1L) diff --git a/R/browse.R b/R/browse.R index 9f60298f3..726a97950 100644 --- a/R/browse.R +++ b/R/browse.R @@ -77,7 +77,10 @@ browse_package <- function(package = NULL) { grl <- set_names(grl$url, nm = grl$remote) parsed <- parse_github_remotes(grl) urls <- c(urls, glue_data(parsed, "https://{host}/{repo_owner}/{repo_name}")) - details <- c(details, map(parsed$name, ~ glue("{ui_value(.x)} remote"))) + details <- c( + details, + map(parsed$name, ~ cli::cli_fmt(cli::cli_text("{.val {.x}} remote"))) + ) } desc_urls_dat <- desc_urls(package, include_cran = TRUE) @@ -86,11 +89,11 @@ browse_package <- function(package = NULL) { details, map( desc_urls_dat$desc_field, - ~ if (is.na(.x)) "CRAN" else glue("{ui_field(.x)} field in DESCRIPTION") + ~ if (is.na(.x)) "CRAN" else cli::cli_fmt(cli::cli_text("{.field {.x}} field in DESCRIPTION")) ) ) if (length(urls) == 0) { - ui_oops("Can't find any URLs") + ui_bullets(c(x = "Can't find any URLs.")) return(invisible(character())) } @@ -171,15 +174,17 @@ github_url <- function(package = NULL) { if (is.null(desc_urls_dat)) { if (is.null(package)) { - ui_stop(" - Project {ui_value(project_name())} has no DESCRIPTION file and \\ - has no GitHub remotes configured - No way to discover URLs") + ui_abort(c( + "Project {.val {project_name()}} has no DESCRIPTION file and + has no GitHub remotes configured.", + "No way to discover URLs." + )) } else { - ui_stop(" - Can't find DESCRIPTION for package {ui_value(package)} locally \\ - or on CRAN - No way to discover URLs") + ui_abort(c( + "Can't find DESCRIPTION for package {.pkg {package}} locally + or on CRAN.", + "No way to discover URLs." + )) } } @@ -191,13 +196,13 @@ github_url <- function(package = NULL) { } if (is.null(package)) { - ui_stop(" - Project {ui_value(project_name())} has no GitHub remotes configured \\ - and has no GitHub URLs in DESCRIPTION") + ui_abort(" + Project {.val {project_name()}} has no GitHub remotes configured + and has no GitHub URLs in DESCRIPTION.") } - ui_warn(" - Package {ui_value(package)} has no GitHub URLs in DESCRIPTION - Trying the GitHub CRAN mirror") + cli::cli_warn(c( + "!" = "Package {.val {package}} has no GitHub URLs in DESCRIPTION.", + " " = "Trying the GitHub CRAN mirror.")) glue_chr("https://github.com/cran/{package}") } diff --git a/R/ci.R b/R/ci.R index 273046ff1..86c7edec9 100644 --- a/R/ci.R +++ b/R/ci.R @@ -82,7 +82,9 @@ use_circleci_badge <- function(repo_spec = NULL) { circleci_activate <- function(owner, browse = is_interactive()) { url <- glue("https://circleci.com/add-projects/gh/{owner}") - ui_todo("Turn on CircleCI for your repo at {url}") + ui_bullets(c( + "_" = "Turn on CircleCI for your repo at {.url {url}}." + )) if (browse) { utils::browseURL(url) } diff --git a/R/code-of-conduct.R b/R/code-of-conduct.R index 51f38ad6e..95ecd02aa 100644 --- a/R/code-of-conduct.R +++ b/R/code-of-conduct.R @@ -23,9 +23,8 @@ #' @export use_code_of_conduct <- function(contact, path = NULL) { if (missing(contact)) { - ui_stop(" - {ui_code('use_code_of_conduct()')} requires contact details in \\ - first argument") + ui_abort(" + {.fun use_code_of_conduct} requires contact details in first argument.") } new <- use_coc(contact = contact, path = path) @@ -35,13 +34,16 @@ use_code_of_conduct <- function(contact, path = NULL) { href <- sub("/$", "", href) href <- paste0(href, "/CODE_OF_CONDUCT.html") - ui_todo("You may also want to describe the code of conduct in your README:") - ui_code_block(" + ui_bullets(c( + "_" = "You may also want to describe the code of conduct in your README:" + )) + ui_code_snippet(" ## Code of Conduct Please note that the {project_name()} project is released with a \\ [Contributor Code of Conduct]({href}). By contributing to this project, \\ - you agree to abide by its terms." + you agree to abide by its terms.", + language = "" ) invisible(new) diff --git a/R/course.R b/R/course.R index ab3a1ed8f..772743717 100644 --- a/R/course.R +++ b/R/course.R @@ -63,19 +63,20 @@ use_course <- function(url, destdir = getOption("usethis.destdir")) { check_path_is_directory(destdir) if (destdir_not_specified && is_interactive()) { - ui_line(c( - "Downloading into {ui_path(destdir)}.", - "Prefer a different location? Cancel, try again, and specify {ui_code('destdir')}" + ui_bullets(c( + "i" = "Downloading into {.path {pth(destdir)}}.", + "_" = "Prefer a different location? Cancel, try again, and specify + {.arg destdir}." )) - if (ui_nope("OK to proceed?")) { - ui_oops("Cancelling download.") + if (ui_nah("OK to proceed?")) { + ui_bullets(c(x = "Cancelling download.")) return(invisible()) } } - ui_done("Downloading from {ui_value(url)}") + ui_bullets(c("v" = "Downloading from {.url {url}}.")) zipfile <- tidy_download(url, destdir) - ui_done("Download stored in {ui_path(zipfile)}") + ui_bullets(c("v" = "Download stored in {.path {pth(zipfile)}}.")) check_is_zip(attr(zipfile, "content-type")) tidy_unzip(zipfile, cleanup = NA) } @@ -90,9 +91,9 @@ use_zip <- function(url, cleanup = if (rlang::is_interactive()) NA else FALSE) { url <- normalize_url(url) check_path_is_directory(destdir) - ui_done("Downloading from {ui_value(url)}") + ui_bullets(c("v" = "Downloading from {.url {url}}.")) zipfile <- tidy_download(url, destdir) - ui_done("Download stored in {ui_path(zipfile)}") + ui_bullets(c("v" = "Download stored in {.path {pth(zipfile)}}.")) check_is_zip(attr(zipfile, "content-type")) tidy_unzip(zipfile, cleanup) } @@ -255,14 +256,15 @@ tidy_download <- function(url, destdir = getwd()) { tmp <- file_temp("tidy-download-") h <- download_url(url, destfile = tmp) - ui_line() + cli::cat_line() cd <- content_disposition(h) base_name <- make_filename(cd, fallback = path_file(url)) full_path <- path(destdir, base_name) if (!can_overwrite(full_path)) { - ui_stop("Cancelling download, to avoid overwriting {ui_path(full_path)}") + ui_abort(" + Cancelling download, to avoid overwriting {.path {pth(full_path)}}.") } attr(full_path, "content-type") <- content_type(h) attr(full_path, "content-disposition") <- cd @@ -303,12 +305,12 @@ download_url <- function(url, status <- try_download(url, destfile, handle = handle) if (inherits(status, "error") && is_interactive()) { - ui_oops(status$message) - if (ui_nope(" - Download failed :( - See above for everything we know about why it failed. - Shall we try a couple more times, with a longer timeout? - ")) { + ui_bullets(c("x" = status$message)) + if (ui_nah(c( + "!" = "Download failed :(", + "i" = "See above for everything we know about why it failed.", + " " = "Shall we try a couple more times, with a longer timeout?" + ))) { n_tries <- 1 } } @@ -323,7 +325,7 @@ download_url <- function(url, ) } i <- i + 1 - ui_info("Retrying download ... attempt {i}") + ui_bullets(c("i" = "Retrying download ... attempt {i}.")) status <- try_download(url, destfile, handle = handle) } @@ -368,28 +370,30 @@ tidy_unzip <- function(zipfile, cleanup = FALSE) { target <- path(base_path, td) utils::unzip(zipfile, files = filenames, exdir = base_path) } - ui_done( - "Unpacking ZIP file into {ui_path(target, base_path)} \\ - ({length(filenames)} files extracted)" - ) + ui_bullets(c( + "v" = "Unpacking ZIP file into {.path {pth(target, base_path)}} + ({length(filenames)} file{?s} extracted)." + )) if (isNA(cleanup)) { cleanup <- is_interactive() && - ui_yeah("Shall we delete the ZIP file ({ui_path(zipfile, base_path)})?") + ui_yep("Shall we delete the ZIP file ({.path {pth(zipfile, base_path)}})?") } if (isTRUE(cleanup)) { - ui_done("Deleting {ui_path(zipfile, base_path)}") + ui_bullets(c("v" = "Deleting {.path {pth(zipfile, base_path)}}.")) file_delete(zipfile) } if (is_interactive()) { rproj_path <- rproj_paths(target) if (length(rproj_path) == 1 && rstudioapi::hasFun("openProject")) { - ui_done("Opening project in RStudio") + ui_bullets(c("v" = "Opening project in RStudio.")) rstudioapi::openProject(target, newSession = TRUE) } else if (!in_rstudio_server()) { - ui_done("Opening {ui_path(target, base_path)} in the file manager") + ui_bullets(c( + "v" = "Opening {.path {pth(target, base_path)}} in the file manager." + )) utils::browseURL(path_real(target)) } } @@ -462,10 +466,10 @@ modify_github_url <- function(url) { } hopeless_url <- function(url) { - ui_info( - "URL does not match a recognized form for Google Drive or DropBox. \\ - No change made." - ) + ui_bullets(c( + "!" = "URL does not match a recognized form for Google Drive or DropBox; + no change made." + )) url } @@ -552,9 +556,9 @@ check_is_zip <- function(ct) { # see https://github.com/r-lib/usethis/issues/573 allowed <- c("application/zip", "application/x-zip-compressed") if (!ct %in% allowed) { - ui_stop(c( - "Download does not have MIME type {ui_value('application/zip')}.", - "Instead it's {ui_value(ct)}." + ui_abort(c( + "Download does not have MIME type {.val application/zip}.", + "Instead it's {.val {ct}}." )) } invisible(ct) @@ -568,9 +572,9 @@ check_is_zip <- function(ct) { # http://test.greenbytes.de/tech/tc2231/ parse_content_disposition <- function(cd) { if (!grepl("^attachment;", cd)) { - ui_stop(c( - "{ui_code('Content-Disposition')} header doesn't start with {ui_value('attachment')}.", - "Actual header: {ui_value(cd)}" + ui_abort(c( + "{.code Content-Disposition} header doesn't start with {.val attachment}.", + "Actual header: {.val cd}" )) } diff --git a/R/coverage.R b/R/coverage.R index 1af822d92..dc9dd5f2a 100644 --- a/R/coverage.R +++ b/R/coverage.R @@ -16,7 +16,9 @@ use_coverage <- function(type = c("codecov", "coveralls"), repo_spec = NULL) { return(invisible(FALSE)) } } else if (type == "coveralls") { - ui_todo("Turn on coveralls for this repo at https://coveralls.io/repos/new") + ui_bullets(c( + "_" = "Turn on coveralls for this repo at {.url https://coveralls.io/repos/new}." + )) } switch( @@ -25,9 +27,10 @@ use_coverage <- function(type = c("codecov", "coveralls"), repo_spec = NULL) { coveralls = use_coveralls_badge(repo_spec) ) - ui_todo(" - Call {ui_code('use_github_action(\"test-coverage\")')} to continuously \\ - monitor test coverage.") + ui_bullets(c( + "_" = "Call {.code use_github_action(\"test-coverage\")} to continuously + monitor test coverage." + )) invisible(TRUE) } diff --git a/R/cpp11.R b/R/cpp11.R index 0d93fd075..9162b917c 100644 --- a/R/cpp11.R +++ b/R/cpp11.R @@ -37,6 +37,8 @@ check_cpp_register_deps <- function() { installed <- map_lgl(cpp_register_deps, is_installed) if (!all(installed)) { - ui_todo("Now install {ui_value(cpp_register_deps[!installed])} to use cpp11.") + ui_bullets(c( + "_" = "Now install {.pkg {cpp_register_deps[!installed]}} to use {.pkg cpp11}." + )) } } diff --git a/R/create.R b/R/create.R index 4616a9cc6..5b264ad0c 100644 --- a/R/create.R +++ b/R/create.R @@ -87,9 +87,11 @@ create_project <- function(path, if (rstudio) { use_rstudio() } else { - ui_done("Writing a sentinel file {ui_path('.here')}") - ui_todo("Build robust paths within your project via {ui_code('here::here()')}") - ui_todo("Learn more at ") + ui_bullets(c( + "v" = "Writing a sentinel file {.path {pth('.here')}}.", + "_" = "Build robust paths within your project via {.fun here::here}.", + "i" = "Learn more at {.url https://here.r-lib.org}." + )) file_create(proj_path(".here")) } @@ -206,26 +208,26 @@ create_from_github <- function(repo_spec, hint <- code_hint_with_host("gh_token_help", host) if (no_auth && is.na(fork)) { - ui_stop(" - Unable to discover a GitHub personal access token - Therefore, can't determine your permissions on {ui_value(repo_spec)} - Therefore, can't decide if `fork` should be `TRUE` or `FALSE` - - You have two choices: - 1. Make your token available (if in doubt, DO THIS): - - Call {ui_code(hint)} for directions - 2. Call {ui_code('create_from_github()')} again, but with \\ - {ui_code('fork = FALSE')} - - Only do this if you are absolutely sure you don't want to fork - - Note you will NOT be in a position to make a pull request") + ui_abort(c( + "x" = "Unable to discover a GitHub personal access token.", + "x" = "Therefore, can't determine your permissions on {.val {repo_spec}}.", + "x" = "Therefore, can't decide if {.arg fork} should be {.code TRUE} or {.code FALSE}.", + "", + "i" = "You have two choices:", + "_" = "Make your token available (if in doubt, DO THIS):", + " " = "Call {.code {hint}} for instructions that should help.", + "_" = "Call {.fun create_from_github} again, but with {.code fork = FALSE}.", + " " = "Only do this if you are absolutely sure you don't want to fork.", + " " = "Note you will NOT be in a position to make a pull request." + )) } if (no_auth && isTRUE(fork)) { - ui_stop(" - Unable to discover a GitHub personal access token - A token is required in order to fork {ui_value(repo_spec)} - - Call {ui_code(hint)} for help configuring a token") + ui_abort(c( + "x" = "Unable to discover a GitHub personal access token.", + "i" = "A token is required in order to fork {.val {repo_spec}}.", + "_" = "Call {.code {hint}} for help configuring a token." + )) } # one of these is true: # - gh is discovering a token for `host` @@ -246,14 +248,14 @@ create_from_github <- function(repo_spec, if (is.na(fork)) { fork <- !isTRUE(repo_info$permissions$push) fork_status <- glue("fork = {fork}") - ui_done("Setting {ui_code(fork_status)}") + ui_bullets(c("v" = "Setting {.code {fork_status}}.")) } # fork is either TRUE or FALSE if (fork && identical(user, repo_info$owner$login)) { - ui_stop(" - Can't fork, because the authenticated user {ui_value(user)} \\ - already owns the source repo {ui_value(repo_info$full_name)}") + ui_abort(" + Can't fork, because the authenticated user {.val {user}} already owns the + source repo {.val {repo_info$full_name}}.") } destdir <- user_path_prep(destdir %||% conspicuous_place()) @@ -265,14 +267,14 @@ create_from_github <- function(repo_spec, if (fork) { ## https://developer.github.com/v3/repos/forks/#create-a-fork - ui_done("Forking {ui_value(repo_info$full_name)}") + ui_bullets(c("v" = "Forking {.val {repo_info$full_name}}.")) upstream_url <- switch( protocol, https = repo_info$clone_url, ssh = repo_info$ssh_url ) repo_info <- gh("POST /repos/{owner}/{repo}/forks") - ui_done("Waiting for the fork to finalize before cloning") + ui_bullets(c("i" = "Waiting for the fork to finalize before cloning...")) Sys.sleep(3) } @@ -282,7 +284,9 @@ create_from_github <- function(repo_spec, ssh = repo_info$ssh_url ) - ui_done("Cloning repo from {ui_value(origin_url)} into {ui_value(repo_path)}") + ui_bullets(c( + "v" = "Cloning repo from {.val {origin_url}} into {.path {repo_path}}." + )) gert::git_clone(origin_url, repo_path, verbose = FALSE) proj_path <- find_rstudio_root(repo_path) @@ -291,16 +295,19 @@ create_from_github <- function(repo_spec, # 2023-01-28 again, it would be more natural to trust the default branch of # the fork, but that cannot always be trusted. For now, we're still using # the default branch learned from the source repo. - ui_info("Default branch is {ui_value(default_branch)}") + ui_bullets(c("i" = "Default branch is {.val {default_branch}}.")) if (fork) { - ui_done("Adding {ui_value('upstream')} remote: {ui_value(upstream_url)}") + ui_bullets(c( + "v" = "Adding {.val upstream} remote: {.val {upstream_url}}" + )) use_git_remote("upstream", upstream_url) pr_merge_main() upstream_remref <- glue("upstream/{default_branch}") - ui_done(" - Setting remote tracking branch for local {ui_value(default_branch)} \\ - branch to {ui_value(upstream_remref)}") + ui_bullets(c( + "v" = "Setting remote tracking branch for local {.val {default_branch}} + branch to {.val {upstream_remref}}." + )) gert::git_branch_set_upstream(upstream_remref, repo = git_repo()) config_key <- glue("remote.upstream.created-by") gert::git_config_set(config_key, "usethis::create_from_github", repo = git_repo()) @@ -344,15 +351,15 @@ challenge_nested_project <- function(path, name) { return(invisible()) } - ui_line( - "New project {ui_value(name)} is nested inside an existing project \\ - {ui_path(path)}, which is rarely a good idea. - If this is unexpected, the here package has a function, \\ - {ui_code('here::dr_here()')} that reveals why {ui_path(path)} \\ - is regarded as a project." - ) - if (ui_nope("Do you want to create anyway?")) { - ui_stop("Cancelling project creation.") + ui_bullets(c( + "!" = "New project {.val {name}} is nested inside an existing project + {.path {pth(path)}}, which is rarely a good idea.", + "i" = "If this is unexpected, the {.pkg here} package has a function, + {.fun here::dr_here} that reveals why {.path {pth(path)}} is regarded + as a project." + )) + if (ui_nah("Do you want to create anyway?")) { + ui_abort("Cancelling project creation.") } invisible() } @@ -368,12 +375,13 @@ challenge_home_directory <- function(path) { } else { "" } - ui_line(" - {ui_path(path)} is {qualification}your home directory. - It is generally a bad idea to create a new project here. - You should probably create your new project in a subdirectory.") - if (ui_nope("Do you want to create anyway?")) { - ui_stop("Good move! Cancelling project creation.") + ui_bullets(c( + "!" = "{.path {pth(path)}} is {qualification}your home directory.", + "i" = "It is generally a bad idea to create a new project here.", + "i" = "You should probably create your new project in a subdirectory." + )) + if (ui_nah("Do you want to create anyway?")) { + ui_abort("Good move! Cancelling project creation.") } invisible() } diff --git a/R/data-table.R b/R/data-table.R index 20ae78e96..1353590d2 100644 --- a/R/data-table.R +++ b/R/data-table.R @@ -20,9 +20,11 @@ use_data_table <- function() { deps <- desc$get_deps() if (any(deps$type == "Depends" & deps$package == "data.table")) { - ui_warn("data.table should be in Imports or Suggests, not Depends") - ui_done("Deleting data.table from {ui_field('Depends')}") - + ui_bullets(c( + "!" = "{.pkg data.table} should be in {.field Imports} or + {.field Suggests}, not {.field Depends}!", + "v" = "Removing {.pkg data.table} from {.field Depends}." + )) desc$del_dep("data.table", "Depends") desc$write() } diff --git a/R/data.R b/R/data.R index 9eca4db20..75069e708 100644 --- a/R/data.R +++ b/R/data.R @@ -59,16 +59,21 @@ use_data <- function(..., desc <- proj_desc() if (!desc$has_fields("LazyData")) { - ui_done("Setting {ui_field('LazyData')} to \\ - {ui_value('true')} in {ui_path('DESCRIPTION')}") + ui_bullets(c( + "v" = "Setting {.field LazyData} to {.val true} in {.path DESCRIPTION}.")) desc$set(LazyData = "true") desc$write() } } check_files_absent(proj_path(paths), overwrite = overwrite) - ui_done("Saving {ui_value(unlist(objs))} to {ui_value(paths)}") - if (!internal) ui_todo("Document your data (see {ui_value('https://r-pkgs.org/data.html')})") + ui_bullets(c( + "v" = "Saving {.val {unlist(objs)}} to {.val {paths}}.")) + if (!internal) { + ui_bullets(c( + "_" = "Document your data (see {.url https://r-pkgs.org/data.html})." + )) + } envir <- parent.frame() mapply( @@ -83,19 +88,21 @@ use_data <- function(..., get_objs_from_dots <- function(.dots) { if (length(.dots) == 0L) { - ui_stop("Nothing to save.") + ui_abort("Nothing to save.") } is_name <- vapply(.dots, is.symbol, logical(1)) if (any(!is_name)) { - ui_stop("Can only save existing named objects.") + ui_abort("Can only save existing named objects.") } objs <- vapply(.dots, as.character, character(1)) duplicated_objs <- which(stats::setNames(duplicated(objs), objs)) if (length(duplicated_objs) > 0L) { objs <- unique(objs) - ui_warn("Saving duplicates only once: {ui_value(names(duplicated_objs))}") + ui_bullets(c( + "!" = "Saving duplicates only once: {.val {names(duplicated_objs)}}." + )) } objs } @@ -111,12 +118,10 @@ check_files_absent <- function(paths, overwrite) { return() } - ui_stop( - " - {ui_path(paths[!ok])} already exist., - Use {ui_code('overwrite = TRUE')} to overwrite. - " - ) + ui_abort(c( + "{.path {pth(paths[!ok])}} already exist.", + "Use {.code overwrite = TRUE} to overwrite." + )) } #' @param name Name of the dataset to be prepared for inclusion in the package. @@ -140,6 +145,8 @@ use_data_raw <- function(name = "DATASET", open = rlang::is_interactive()) { open = open ) - ui_todo("Finish the data preparation script in {ui_value(r_path)}") - ui_todo("Use {ui_code('usethis::use_data()')} to add prepared data to package") + ui_bullets(c( + "_" = "Finish writing the data preparation script in {.path {pth(r_path)}}.", + "_" = "Use {.code usethis::use_data()} to add prepared data to package." + )) } diff --git a/R/description.R b/R/description.R index bf62ca184..6c11d860d 100644 --- a/R/description.R +++ b/R/description.R @@ -131,12 +131,12 @@ usethis_description_defaults <- function(package = NULL) { check_package_name <- function(name) { if (!valid_package_name(name)) { - ui_stop(c( - "{ui_value(name)} is not a valid package name. To be allowed on CRAN, it should:", - "* Contain only ASCII letters, numbers, and '.'", - "* Have at least two characters", - "* Start with a letter", - "* Not end with '.'" + ui_abort(c( + "x" = "{.val {name}} is not a valid package name. To be allowed on CRAN, it should:", + "*" = "Contain only ASCII letters, numbers, and '.'.", + "*" = "Have at least two characters.", + "*" = "Start with a letter.", + "*" = "Not end with '.'." )) } } diff --git a/R/directory.R b/R/directory.R index 239b8a67a..3ec9d5046 100644 --- a/R/directory.R +++ b/R/directory.R @@ -26,17 +26,17 @@ create_directory <- function(path) { if (dir_exists(path)) { return(invisible(FALSE)) } else if (file_exists(path)) { - ui_stop("{ui_path(path)} exists but is not a directory.") + ui_abort("{.path {pth(path)}} exists but is not a directory.") } dir_create(path, recurse = TRUE) - ui_done("Creating {ui_path(path)}") + ui_bullets(c("v" = "Creating {.path {pth(path)}}.")) invisible(TRUE) } check_path_is_directory <- function(path) { if (!file_exists(path)) { - ui_stop("Directory {ui_path(path)} does not exist.") + ui_abort("Directory {.path {pth(path)}} does not exist.") } if (is_link(path)) { @@ -44,7 +44,7 @@ check_path_is_directory <- function(path) { } if (!is_dir(path)) { - ui_stop("{ui_path(path)} is not a directory.") + ui_abort("{.path {pth(path)}} is not a directory.") } } @@ -58,7 +58,7 @@ directory_has_files <- function(x) { check_directory_is_empty <- function(x) { if (directory_has_files(x)) { - ui_stop("{ui_path(x)} exists and is not an empty directory.") + ui_abort("{.path {pth(path)}} exists and is not an empty directory.") } invisible(x) } diff --git a/R/edit.R b/R/edit.R index 84de7ae59..58b025d12 100644 --- a/R/edit.R +++ b/R/edit.R @@ -24,11 +24,11 @@ edit_file <- function(path, open = rlang::is_interactive()) { file_create(path) if (!open) { - ui_todo("Edit {ui_path(path)}") + ui_bullets(c("_" = "Edit {.path {pth(path)}}.")) return(invisible(path)) } - ui_todo("Modify {ui_path(path)}") + ui_bullets(c("_" = "Modify {.path {pth(path)}}.")) if (rstudio_available() && rstudioapi::hasFun("navigateToFile")) { rstudioapi::navigateToFile(path) } else { @@ -45,7 +45,9 @@ edit_template <- function(template = NULL, open = rlang::is_interactive()) { check_is_package("edit_template()") if (is.null(template)) { - ui_info("No template specified... checking {ui_path('inst/templates')}") + ui_bullets(c( + "!" = "No template specified ... checking {.path {pth('inst/templates')}}." + )) template <- choose_template() } @@ -110,7 +112,7 @@ NULL edit_r_profile <- function(scope = c("user", "project")) { path <- scoped_path_r(scope, ".Rprofile", envvar = "R_PROFILE_USER") edit_file(path) - ui_todo("Restart R for changes to take effect") + ui_bullets(c("_" = "Restart R for changes to take effect.")) invisible(path) } @@ -119,7 +121,7 @@ edit_r_profile <- function(scope = c("user", "project")) { edit_r_environ <- function(scope = c("user", "project")) { path <- scoped_path_r(scope, ".Renviron", envvar = "R_ENVIRON_USER") edit_file(path) - ui_todo("Restart R for changes to take effect") + ui_bullets(c("_" = "Restart R for changes to take effect.")) invisible(path) } @@ -159,15 +161,17 @@ edit_rstudio_snippets <- function(type = c( if (new_rstudio && file_exists(old_path) && !file_exists(new_path)) { create_directory(path_dir(new_path)) file_copy(old_path, new_path) - ui_done("Copying snippets file to {ui_path(new_path)}") + ui_bullets(c( + "v" = "Copying snippets file to {.path {pth(new_path)}}." + )) } path <- if (new_rstudio) new_path else old_path if (!file_exists(path)) { - ui_done("New snippet file at {ui_path(path)}") - ui_info(c( - "This masks the default snippets for {ui_field(type)}.", - "Delete this file and restart RStudio to restore the default snippets." + ui_bullets(c( + "v" = "New snippet file at {.path {pth(path)}}.", + "i" = "This masks the default snippets for {.field {type}}.", + "i" = "Delete this file and restart RStudio to restore the default snippets." )) } edit_file(path) @@ -179,7 +183,7 @@ edit_rstudio_prefs <- function() { path <- rstudio_config_path("rstudio-prefs.json") edit_file(path) - ui_todo("Restart RStudio for changes to take effect") + ui_bullets(c("_" = "Restart RStudio for changes to take effect.")) invisible(path) } @@ -248,7 +252,7 @@ git_ignore_path <- function(scope = c("user", "project")) { edit_pkgdown_config <- function() { path <- pkgdown_config_path() if (is.null(path)) { - ui_oops("No pkgdown config file found in current Project.") + ui_bullets(c("x" = "No pkgdown config file found in current Project.")) } else { invisible(edit_file(path)) } diff --git a/R/git-default-branch.R b/R/git-default-branch.R index 2f7abf9ec..fdcaac79d 100644 --- a/R/git-default-branch.R +++ b/R/git-default-branch.R @@ -107,15 +107,16 @@ git_default_branch <- function() { # 404s critique_remote <- function(remote) { if (remote$is_configured && is.na(remote$default_branch)) { - ui_oops(" - The {ui_value(remote$name)} remote is configured, but we can't \\ - determine its default branch. - Possible reasons: - - The remote repo no longer exists, suggesting the local remote should - be deleted. - - We are offline or that specific Git server is down. - - You don't have the necessary permission or something is wrong with - your credentials.") + ui_bullets(c( + "x" = "The {.val {remote$name}} remote is configured, but we can't + determine its default branch.", + " " = "Possible reasons:", + "*" = "The remote repo no longer exists, suggesting the local remote + should be deleted.", + "*" = "We are offline or that specific Git server is down.", + "*" = "You don't have the necessary permission or something is wrong + with your credentials." + )) } } @@ -142,19 +143,19 @@ git_default_branch <- function() { if (is.na(db_local_with_source) ) { if (length(db_source)) { - usethis_abort(c( - "Default branch mismatch between local repo and remote.", - "The default branch of the {.val {db_source$name}} remote is - {.val {db_source$default_branch}}.", - "But the local repo has no branch named - {.val {db_source$default_branch}}.", - "Call {.code git_default_branch_rediscover()} to resolve this." + ui_abort(c( + "x" = "Default branch mismatch between local repo and remote.", + "i" = "The default branch of the {.val {db_source$name}} remote is + {.val {db_source$default_branch}}.", + " " = "But the local repo has no branch named + {.val {db_source$default_branch}}.", + "_" = "Call {.run [git_default_branch_rediscover()](usethis::git_default_branch_rediscover())} to resolve this." ), class = "error_default_branch", db_source = db_source ) } else { - usethis_abort( + ui_abort( "Can't determine the local repo's default branch.", class = "error_default_branch" ) @@ -170,13 +171,13 @@ git_default_branch <- function() { # we learned a default branch from the source repo and it doesn't match # the local default branch - usethis_abort(c( - "Default branch mismatch between local repo and remote.", - "The default branch of the {.val {db_source$name}} remote is - {.val {db_source$default_branch}}.", - "But the default branch of the local repo appears to be - {.val {db_local_with_source}}.", - "Call {.code git_default_branch_rediscover()} to resolve this." + ui_abort(c( + "x" = "Default branch mismatch between local repo and remote.", + "i" = "The default branch of the {.val {db_source$name}} remote is + {.val {db_source$default_branch}}.", + " " = "But the default branch of the local repo appears to be + {.val {db_local_with_source}}.", + "_" = "Call {.run [git_default_branch_rediscover()](usethis::git_default_branch_rediscover())} to resolve this." ), class = "error_default_branch", db_source = db_source, db_local = db_local_with_source @@ -244,9 +245,10 @@ guess_local_default_branch <- function(prefer = NULL, verbose = FALSE) { gb <- gert::git_branch_list(local = TRUE, repo = repo)[["name"]] if (length(gb) == 0) { - ui_stop(" - Can't find any local branches. - Do you need to make your first commit?") + ui_abort(c( + "x" = "Can't find any local branches.", + " " = "Do you need to make your first commit?" + )) } candidates <- c(prefer, default_branch_candidates()) @@ -258,17 +260,17 @@ guess_local_default_branch <- function(prefer = NULL, verbose = FALSE) { db <- first_matched(gb, candidates) } else { # TODO: perhaps this should be classed, so I can catch it and distinguish - # from the ui_stop() above, where there are no local branches. - ui_stop(" + # from the ui_abort() above, where there are no local branches. + ui_abort(" Unable to guess which existing local branch plays the role of the default.") } if (verbose) { - ui_info(" - Local branch {ui_value(db)} appears to play the role of \\ - the default branch.") + ui_bullets(c( + "i" = "Local branch {.val {db}} appears to play the role of the default + branch." + )) } - db } @@ -281,8 +283,10 @@ guess_local_default_branch <- function(prefer = NULL, verbose = FALSE) { #' } git_default_branch_configure <- function(name = "main") { check_string(name) - ui_done("Configuring {ui_field('init.defaultBranch')} as {ui_value(name)}.") - ui_info("Remember: this only affects repos you create in the future.") + ui_bullets(c( + "v" = "Configuring {.field init.defaultBranch} as {.val {name}}.", + "i" = "Remember: this only affects repos you create in the future!" + )) use_git_config(scope = "user", `init.defaultBranch` = name) invisible(name) } @@ -323,7 +327,7 @@ git_default_branch_rename <- function(from = NULL, to = "main") { if (!is.null(from) && !gert::git_branch_exists(from, local = TRUE, repo = repo)) { - ui_stop("Can't find existing branch named {ui_value(from)}.") + ui_abort("Can't find existing branch named {.val {from}}.") } cfg <- github_remote_config(github_get = TRUE) @@ -332,9 +336,13 @@ git_default_branch_rename <- function(from = NULL, to = "main") { if (cfg$type == "no_github") { from <- from %||% guess_local_default_branch(verbose = TRUE) if (from == to) { - ui_info("Local repo already has {ui_value(from)} as its default branch.") + ui_bullets(c( + "i" = "Local repo already has {.val {from}} as its default branch." + )) } else { - ui_done("Moving local {ui_value(from)} branch to {ui_value(to)}.") + ui_bullets(c( + "v" = "Moving local {.val {from}} branch to {.val {to}}." + )) gert::git_branch_move(branch = from, new_branch = to, repo = repo) rstudio_git_tickle() report_fishy_files(old_name = from, new_name = to) @@ -347,24 +355,24 @@ git_default_branch_rename <- function(from = NULL, to = "main") { old_source_db <- tr$default_branch if (!isTRUE(tr$can_admin)) { - ui_stop(" - You don't seem to have {ui_field('admin')} permissions for the source \\ - repo {ui_value(tr$repo_spec)}, which is required to rename the default \\ - branch.") + ui_abort(" + You don't seem to have {.field admin} permissions for the source repo + {.val {tr$repo_spec}}, which is required to rename the default branch.") } old_local_db <- from %||% guess_local_default_branch(old_source_db, verbose = FALSE) if (old_local_db != old_source_db) { - ui_oops(" - It's weird that the current default branch for your local repo and \\ - the source repo are different: - {ui_value(old_local_db)} (local) != {ui_value(old_source_db)} (source)") - if (ui_nope( + ui_bullets(c( + "!" = "It's weird that the current default branch for your local repo and + the source repo are different:", + " " = "{.val {old_local_db}} (local) != {.val {old_source_db}} (source)" + )) + if (ui_nah( "Are you sure you want to proceed?", yes = "yes", no = "no", shuffle = FALSE)) { - ui_oops("Cancelling.") + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } @@ -380,13 +388,15 @@ git_default_branch_rename <- function(from = NULL, to = "main") { } if (source_update) { - ui_done(" - Default branch of the source repo {ui_value(tr$repo_spec)} has moved: \\ - {ui_value(old_source_db)} --> {ui_value(to)}") + ui_bullets(c( + "v" = "Default branch of the source repo {.val {tr$repo_spec}} has moved:", + " " = "{.val {old_source_db}} {cli::symbol$arrow_right} {.val {to}}" + )) } else { - ui_done(" - Default branch of source repo {ui_value(tr$repo_spec)} is \\ - {ui_value(to)}. Nothing to be done.") + ui_bullets(c( + "i" = "Default branch of source repo {.val {tr$repo_spec}} is + {.val {to}}. Nothing to be done." + )) } report_fishy_files(old_name = old_local_db, new_name = to) @@ -414,7 +424,7 @@ rediscover_default_branch <- function(old_name = NULL, report_on_source = TRUE) repo <- git_repo() if (!is.null(old_name) && !gert::git_branch_exists(old_name, local = TRUE, repo = repo)) { - ui_stop("Can't find existing local branch named {ui_value(old_name)}.") + ui_abort("Can't find existing local branch named {.val {old_name}}.") } cfg <- github_remote_config(github_get = TRUE) @@ -484,27 +494,33 @@ rediscover_default_branch <- function(old_name = NULL, report_on_source = TRUE) } if (report_on_source) { - ui_info(" - Default branch of the source repo {ui_value(tr$repo_spec)}: {ui_value(db)}") + ui_bullets(c( + "i" = "Default branch of the source repo {.val {tr$repo_spec}} is + {.val {db}}." + )) } if (local_update) { - ui_done(" - Default branch of local repo has moved: \\ - {ui_value(old_name)} --> {ui_value(db)}") + ui_bullets(c( + "v" = "Default branch of local repo has moved: + {.val {old_name}} {cli::symbol$arrow_right} {.val {db}}" + )) } else { - ui_done(" - Default branch of local repo is {ui_value(db)}. Nothing to be done.") + ui_bullets(c( + "i" = "Default branch of local repo is {.val {db}}. Nothing to be done." + )) } if (cfg$type == "fork") { if (fork_update) { - ui_done(" - Default branch of your fork has moved: \\ - {ui_value(old_name_fork)} --> {ui_value(db)}") + ui_bullets(c( + "v" = "Default branch of your fork has moved: + {.val {old_name_fork}} {cli::symbol$arrow_right} {.val {db}}" + )) } else { - ui_done(" - Default branch of your fork is {ui_value(db)}. Nothing to be done.") + ui_bullets(c( + "i" = "Default branch of your fork is {.val {db}}. Nothing to be done." + )) } } @@ -515,23 +531,23 @@ challenge_non_default_branch <- function(details = "Are you sure you want to pro default_branch = NULL) { actual <- git_branch() default_branch <- default_branch %||% git_default_branch() - if (nzchar(details)) { - details <- paste0("\n", details) - } if (actual != default_branch) { - if (ui_nope(" - Current branch ({ui_value(actual)}) is not repo's default \\ - branch ({ui_value(default_branch)}).{details}")) { - ui_stop("Cancelling. Not on desired branch.") + if (ui_nah(c( + "!" = "Current branch ({.val {actual}}) is not repo's default branch + ({.val {default_branch}}).", + " " = details + ))) { + ui_abort("Cancelling. Not on desired branch.") } } invisible() } report_fishy_files <- function(old_name = "master", new_name = "main") { - ui_todo(" - Be sure to update files that refer to the default branch by name. - Consider searching within your project for {ui_value(old_name)}.") + ui_bullets(c( + "_" = "Be sure to update files that refer to the default branch by name.", + " " = "Consider searching within your project for {.val {old_name}}." + )) # I don't want failure of a fishy file check to EVER cause # git_default_branch_rename() to fail and prevent the call to # git_default_branch_rediscover() @@ -566,11 +582,14 @@ fishy_github_actions <- function(new_name = "main") { } paths <- sort(paths) - ui_paths <- map_chr(paths, ui_path) - - ui_oops(c( - "These GitHub Action files don't mention the new default branch {ui_value(new_name)}:", - paste0("- ", ui_paths) + ui_paths <- map_chr(paths, ui_path_impl) + + # TODO: the ui_paths used to be a nested bullet list + # if that ever becomes possible/easier with cli, go back to that + ui_bullets(c( + "x" = "{cli::qty(length(ui_paths))}{?No/This/These} GitHub Action file{?s} + {?/doesn't/don't} mention the new default branch {.val {new_name}}:", + " " = "{.path {ui_paths}}" )) invisible(paths) @@ -597,10 +616,10 @@ fishy_badges <- function(old_name = "master") { return(invisible(character())) } - ui_path <- ui_path(proj_rel_path(path)) - ui_oops(c( - "Some badges may refer to the old default branch {ui_value(old_name)}:", - paste0("- ", ui_path) + ui_bullets(c( + "x" = "Some badges appear to refer to the old default branch + {.val {old_name}}.", + "_" = "Check and correct, if needed, in this file: {.path {pth(path)}}" )) invisible(path) @@ -630,10 +649,11 @@ fishy_bookdown_config <- function(old_name = "master") { return(invisible(character())) } - ui_path <- ui_path(proj_rel_path(bookdown_config)) - ui_oops(c( - "The bookdown configuration file may refer to the old default branch {ui_value(old_name)}:", - paste0("- ", ui_path) + ui_bullets(c( + "x" = "The bookdown configuration file may refer to the old default branch + {.val {old_name}}.", + "_" = "Check and correct, if needed, in this file: + {.path {pth(bookdown_config)}}" )) invisible(path) diff --git a/R/git.R b/R/git.R index 80681423d..6596a6449 100644 --- a/R/git.R +++ b/R/git.R @@ -13,7 +13,7 @@ use_git <- function(message = "Initial commit") { needs_init <- !uses_git() if (needs_init) { - ui_done("Initialising Git repo") + ui_bullets(c("v" = "Initialising Git repo.")) git_init() } @@ -23,7 +23,7 @@ use_git <- function(message = "Initial commit") { } if (needs_init) { - restart_rstudio("A restart of RStudio is required to activate the Git pane") + restart_rstudio("A restart of RStudio is required to activate the Git pane.") } invisible(TRUE) @@ -159,7 +159,7 @@ use_git_config <- function(scope = c("user", "project"), ...) { git_protocol <- function() { protocol <- tolower(getOption("usethis.protocol", "unset")) if (identical(protocol, "unset")) { - ui_info("Defaulting to {ui_value('https')} Git protocol") + ui_bullets(c("i" = "Defaulting to {.val https} Git protocol.")) protocol <- "https" } else { check_protocol(protocol) @@ -179,9 +179,7 @@ check_protocol <- function(protocol) { if (!is_string(protocol) || !(tolower(protocol) %in% c("https", "ssh"))) { options(usethis.protocol = NULL) - ui_stop(" - {ui_code('protocol')} must be either {ui_value('https')} or \\ - {ui_value('ssh')}") + ui_abort("{.arg protocol} must be either {.val https} or {.val ssh}.") } invisible() } @@ -251,9 +249,10 @@ use_git_remote <- function(name = "origin", url, overwrite = FALSE) { repo <- git_repo() if (name %in% names(remotes) && !overwrite) { - ui_stop(" - Remote {ui_value(name)} already exists. Use \\ - {ui_code('overwrite = TRUE')} to edit it anyway.") + ui_abort(c( + "Remote {.val {name}} already exists.", + "Use {.code overwrite = TRUE} to edit it anyway." + )) } if (name %in% names(remotes)) { @@ -289,32 +288,22 @@ git_clean <- function() { paths <- st[st$status == "new", ][["file"]] n <- length(paths) if (n == 0) { - ui_info("Found no untracked files") + ui_bullets(c("i" = "Found no untracked files.")) return(invisible()) } paths <- sort(paths) - ui_paths <- map_chr(paths, ui_path) - if (n > 10) { - ui_paths <- c(ui_paths[1:10], "...") - } - - if (n == 1) { - file_hint <- "There is 1 untracked file:" - } else { - file_hint <- "There are {n} untracked files:" - } - ui_line(c( - file_hint, - paste0("* ", ui_paths) + ui_paths <- map_chr(paths, ui_path_impl) + ui_bullets(c( + "i" = "{cli::qty(n)}There {?is/are} {n} untracked file{?s}:", + bulletize(usethis_map_cli(ui_paths, template = "{.file <>}")) )) - if (ui_yeah(" - - Do you want to remove {if (n == 1) 'it' else 'them'}?", + if (ui_yep( + "{cli::qty(n)}Do you want to remove {?it/them}?", yes = "yes", no = "no", shuffle = FALSE)) { file_delete(paths) - ui_done("{n} file(s) deleted") + ui_bullets(c("v" = "{n} file{?s} deleted.")) } rstudio_git_tickle() invisible() @@ -352,11 +341,14 @@ git_sitrep <- function(tool = c("git", "github"), if ("git" %in% tool && "user" %in% scope) { cli::cli_h3("Git global (user)") git_user_sitrep("user") - kv_line("Global (user-level) gitignore file", git_ignore_path("user")) + kv_line( + "Global (user-level) gitignore file", + I("{.path {git_ignore_path('user')}}") + ) vaccinated <- git_vaccinated() kv_line("Vaccinated", vaccinated) if (!vaccinated) { - ui_info("See {ui_code('?git_vaccinate')} to learn more") + ui_bullets(c("i" = "See {.fun usethis::git_vaccinate} to learn more.")) } kv_line("Default Git protocol", git_protocol()) kv_line("Default initial branch name", init_default_branch) @@ -376,22 +368,29 @@ git_sitrep <- function(tool = c("git", "github"), } if (!proj_active()) { - ui_info("No active usethis project") + ui_bullets(c("i" = "No active usethis project.")) return(invisible()) } - cli::cli_h2(glue("Active usethis project: {ui_value(proj_get())}")) + cli::cli_h2("Active usethis project: {.val {proj_get()}}") if (!uses_git()) { - ui_info("Active project is not a Git repo") + ui_bullets(c("i" = "Active project is not a Git repo.")) return(invisible()) } # current branch ------------------------------------------------------------- branch <- tryCatch(git_branch(), error = function(e) NULL) tracking_branch <- if (is.null(branch)) NA_character_ else git_branch_tracking() - # TODO: can't really express with kv_line() helper - branch <- if (is.null(branch)) "" else branch - tracking_branch <- if (is.na(tracking_branch)) "" else tracking_branch + if (is.null(branch)) { + branch <- cli::format_inline(ui_special()) + } else { + branch <- cli::format_inline("{.val {branch}}") + } + if (is.na(tracking_branch)) { + tracking_branch <- cli::format_inline(ui_special()) + } else { + tracking_branch <- cli::format_inline("{.val {tracking_branch}}") + } # local git config ----------------------------------------------------------- if ("git" %in% tool) { @@ -402,9 +401,11 @@ git_sitrep <- function(tool = c("git", "github"), default_branch_sitrep() # vertical alignment would make this nicer, but probably not worth it - ui_bullet(glue(" - Current local branch -> remote tracking branch: - {ui_value(branch)} -> {ui_value(tracking_branch)}")) + ui_bullets(c( + "*" = "Current local branch {cli::symbol$arrow_right} remote tracking + branch:", + " " = "{branch} {cli::symbol$arrow_right} {tracking_branch}" + )) } # GitHub remote config ------------------------------------------------------- @@ -414,7 +415,7 @@ git_sitrep <- function(tool = c("git", "github"), cfg <- github_remote_config() if (cfg$type == "no_github") { - ui_info("Project does not use GitHub") + ui_bullets(c("i" = "Project does not use GitHub.")) return(invisible()) } @@ -426,7 +427,7 @@ git_sitrep <- function(tool = c("git", "github"), cli::cli_text("Project:") } - purrr::walk(format(cfg), ui_bullet) + ui_bullets(format(cfg)) } invisible() @@ -441,7 +442,7 @@ git_user_sitrep <- function(scope = c("user", "project")) { user_local <- git_user_get("local") if (scope == "project" && !all(map_lgl(user_local, is.null))) { - ui_info("This repo has a locally configured user") + ui_bullets(c("i" = "This repo has a locally configured user.")) } kv_line("Name", user$name) @@ -456,47 +457,55 @@ git_user_check <- function(user) { if (all(map_lgl(user, is.null))) { hint <- 'use_git_config(user.name = "", user.email = "")' - ui_oops( - "Git user's name and email are not set. Configure using {ui_code(hint)}." - ) + ui_bullets(c( + "x" = "Git user's name and email are not set.", + "i" = "Configure using {.code {hint}}." + )) return(invisible(NULL)) } if (is.null(user$name)) { hint <- 'use_git_config(user.name = "")' - ui_oops("Git user's name is not set. Configure using {ui_code(hint)}.") + ui_bullets(c( + "x" = "Git user's name is not set.", + "i" = "Configure using {.code {hint}}." + )) } if (is.null(user$email)) { hint <- 'use_git_config(user.email = "")' - ui_oops("Git user's email is not set. Configure using {ui_code(hint)}.") + ui_bullets(c( + "x" = "Git user's email is not set.", + "i" = "Configure using {.code {hint}}." + )) } } -# TODO: when I really overhaul the UI, determine if I can just re-use the -# git_default_branch() error messages in the sitrep -# the main point is converting an error to an "oops" type of message default_branch_sitrep <- function() { tryCatch( kv_line("Default branch", git_default_branch()), error_default_branch = function(e) { if (has_name(e, "db_local")) { # FYI existence of db_local implies existence of db_source - ui_oops(" - Default branch mismatch between local repo and remote! - {ui_value(e$db_source$name)} remote default branch: \\ - {ui_value(e$db_source$default_branch)} - Local default branch: {ui_value(e$db_local)} - Call {ui_code('git_default_branch_rediscover()')} to resolve this.") + ui_bullets(c( + "x" = "Default branch mismatch between local repo and remote.", + "i" = "The default branch of the {.val {e$db_source$name}} remote is + {.val {e$db_source$default_branch}}.", + "!" = "The local repo has no branch named + {.val {e$db_source$default_branch}}.", + "_" = "Call {.run [git_default_branch_rediscover()](usethis::git_default_branch_rediscover())} to resolve this." + )) } else if (has_name(e, "db_source")) { - ui_oops(" - Default branch mismatch between local repo and remote! - {ui_value(e$db_source$name)} remote default branch: \\ - {ui_value(e$db_source$default_branch)} - Local repo has no branch by that name nor any other obvious candidates. - Call {ui_code('git_default_branch_rediscover()')} to resolve this.") + ui_bullets(c( + "x" = "Default branch mismatch between local repo and remote.", + "i" = "The default branch of the {.val {e$db_source$name}} remote is + {.val {e$db_source$default_branch}}.", + "!" = "The local repo has no branch by that name, nor any other + obvious candidates.", + "_" = "Call {.run [git_default_branch_rediscover()](usethis::git_default_branch_rediscover())} to resolve this." + )) } else { - ui_oops("Default branch cannot be determined.") + ui_bullets(c("Default branch cannot be determined.")) } } ) @@ -506,18 +515,21 @@ default_branch_sitrep <- function() { #' Vaccinate your global gitignore file #' -#' Adds `.Rproj.user`, `.Rhistory`, `.Rdata`, `.httr-oauth`, `.DS_Store`, and `.quarto` to -#' your global (a.k.a. user-level) `.gitignore`. This is good practice as it -#' decreases the chance that you will accidentally leak credentials to GitHub. -#' `git_vaccinate()` also tries to detect and fix the situation where you have a -#' global gitignore file, but it's missing from your global Git config. +#' Adds `.Rproj.user`, `.Rhistory`, `.Rdata`, `.httr-oauth`, `.DS_Store`, and +#' `.quarto` to your global (a.k.a. user-level) `.gitignore`. This is good +#' practice as it decreases the chance that you will accidentally leak +#' credentials to GitHub. `git_vaccinate()` also tries to detect and fix the +#' situation where you have a global gitignore file, but it's missing from your +#' global Git config. #' #' @export git_vaccinate <- function() { ensure_core_excludesFile() path <- git_ignore_path(scope = "user") if (!file_exists(path)) { - ui_done("Creating the global (user-level) gitignore: {ui_path(path)}") + ui_bullets(c( + "v" = "Creating the global (user-level) gitignore: {.path {pth(path)}}" + )) } write_union(path, git_ignore_lines) } diff --git a/R/github-actions.R b/R/github-actions.R index 846c3a508..c60a78ad0 100644 --- a/R/github-actions.R +++ b/R/github-actions.R @@ -121,7 +121,7 @@ use_github_action <- function(name = NULL, } if (!is.null(readme)) { - ui_todo("Learn more at <{readme}>.") + ui_bullets(c("_" = "Learn more at {.url {readme}}.")) } badge <- badge %||% is_check_action(url) @@ -227,11 +227,9 @@ use_tidy_github_actions <- function(ref = NULL) { has_appveyor_travis <- file_exists(old_configs) if (any(has_appveyor_travis)) { - if (ui_yeah( - "Remove existing {ui_path('.travis.yml')} and {ui_path('appveyor.yml')}?" - )) { + if (ui_yep("Remove existing {.path .travis.yml} and {.path appveyor.yml}?")) { file_delete(old_configs[has_appveyor_travis]) - ui_todo("Remove old badges from README") + ui_bullets(c("_" = "Remove old badges from README.")) } } @@ -250,10 +248,10 @@ check_uses_github_actions <- function() { return(invisible()) } - ui_stop(" - Cannot detect that package {ui_value(project_name())} already \\ - uses GitHub Actions. - Do you need to run {ui_code('use_github_actions()')}?") + ui_abort(c( + "Cannot detect that package {.pkg {project_name()}} already uses GitHub Actions.", + "Do you need to run {.fun use_github_action}?" + )) } latest_release <- function(repo_spec = "https://github.com/r-lib/actions") { diff --git a/R/github-labels.R b/R/github-labels.R index 2da10a7e7..cb5a37e3b 100644 --- a/R/github-labels.R +++ b/R/github-labels.R @@ -109,11 +109,16 @@ use_github_labels <- function(repo_spec = deprecated(), cur_label_names <- label_attr("name", cur_labels) to_rename <- intersect(cur_label_names, names(rename)) if (length(to_rename) > 0) { - delta <- purrr::map2_chr( - to_rename, rename[to_rename], - ~ paste0(ui_value(.x), " -> ", ui_value(.y)) + dat <- data.frame(from = to_rename, to = rename[to_rename]) + delta <- glue_data( + dat, + "{.val <>} {cli::symbol$arrow_right} {.val <>}", + .open = "<<", .close = ">>" ) - ui_done("Renaming labels: {paste0(delta, collapse = '\n')}") + ui_bullets(c( + "v" = "Renaming labels:", + bulletize(delta) + )) # Can't do this at label level, i.e. "old_label_name --> new_label_name" # Fails if "new_label_name" already exists @@ -153,7 +158,7 @@ use_github_labels <- function(repo_spec = deprecated(), ) labels <- union(labels, setdiff(rename, cur_label_names)) } else { - ui_info("No labels need renaming") + ui_bullets(c("i" = "No labels need renaming.")) } cur_labels <- gh("GET /repos/{owner}/{repo}/labels") @@ -161,10 +166,13 @@ use_github_labels <- function(repo_spec = deprecated(), # Add missing labels if (all(labels %in% cur_label_names)) { - ui_info("No new labels needed") + ui_bullets(c("i" = "No new labels needed.")) } else { to_add <- setdiff(labels, cur_label_names) - ui_done("Adding missing labels: {ui_value(to_add)}") + ui_bullets(c( + "v" = "Adding missing labels:", + bulletize(usethis_map_cli(to_add)) + )) for (label in to_add) { gh( @@ -184,10 +192,13 @@ use_github_labels <- function(repo_spec = deprecated(), label_attr("color", cur_labels), cur_label_names ) if (identical(cur_label_colours[names(colours)], colours)) { - ui_info("Label colours are up-to-date") + ui_bullets(c("i" = "Label colours are up-to-date.")) } else { to_update <- intersect(cur_label_names, names(colours)) - ui_done("Updating colours: {ui_value(to_update)}") + ui_bullets(c( + "v" = "Updating colours:", + bulletize(usethis_map_cli(to_update)) + )) for (label in to_update) { gh( @@ -203,10 +214,13 @@ use_github_labels <- function(repo_spec = deprecated(), label_attr("description", cur_labels), cur_label_names ) if (identical(cur_label_descriptions[names(descriptions)], descriptions)) { - ui_info("Label descriptions are up-to-date") + ui_bullets(c("i" = "Label descriptions are up-to-date.")) } else { to_update <- intersect(cur_label_names, names(descriptions)) - ui_done("Updating descriptions: {ui_value(to_update)}") + ui_bullets(c( + "v" = "Updating descriptions:", + bulletize(usethis_map_cli(to_update)) + )) for (label in to_update) { gh( @@ -223,12 +237,17 @@ use_github_labels <- function(repo_spec = deprecated(), to_remove <- setdiff(cur_label_names[default], labels) if (length(to_remove) > 0) { - ui_done("Removing default labels: {ui_value(to_remove)}") + ui_bullets(c( + "v" = "Removing default labels:", + bulletize(usethis_map_cli(to_remove)) + )) for (label in to_remove) { issues <- gh("GET /repos/{owner}/{repo}/issues", labels = label) if (length(issues) > 0) { - ui_todo("Delete {ui_value(label)} label manually; it has associated issues") + ui_bullets(c( + "_" = "Delete {.val {label}} label manually; it has associated issues." + )) } else { gh("DELETE /repos/{owner}/{repo}/labels/{name}", name = label) } diff --git a/R/github-pages.R b/R/github-pages.R index 0b43a8687..e5e68a14c 100644 --- a/R/github-pages.R +++ b/R/github-pages.R @@ -71,7 +71,9 @@ use_github_pages <- function(branch = "gh-pages", path = "/", cname = NA) { site <- safe_gh("GET /repos/{owner}/{repo}/pages")[["result"]] if (is.null(site)) { - ui_done("Activating GitHub Pages for {ui_value(tr$repo_spec)}") + ui_bullets(c( + "v" = "Activating GitHub Pages for {.val {tr$repo_spec}}." + )) site <- gh( "POST /repos/{owner}/{repo}/pages", source = list(branch = branch, path = path), @@ -103,7 +105,7 @@ use_github_pages <- function(branch = "gh-pages", path = "/", cname = NA) { site <- safe_gh("GET /repos/{owner}/{repo}/pages")[["result"]] } - ui_done("GitHub Pages is publishing from:") + ui_bullets(c("v" = "GitHub Pages is publishing from:")) if (!is.null(site$cname)) { kv_line("Custom domain", site$cname) } @@ -129,9 +131,10 @@ create_gh_pages_branch <- function(tr, branch = "gh-pages") { return(FALSE) } - ui_done(" - Initializing empty, orphan {ui_value(branch)} branch in GitHub repo \\ - {ui_value(tr$repo_spec)}") + ui_bullets(c( + "v" = "Initializing empty, orphan branch {.val {branch}} in GitHub repo + {.val {tr$repo_spec}}." + )) # GitHub no longer allows you to directly create an empty tree # hence this roundabout method of getting an orphan branch with no files diff --git a/R/github.R b/R/github.R index 818366202..f1f2a615c 100644 --- a/R/github.R +++ b/R/github.R @@ -80,7 +80,7 @@ use_github <- function(organisation = NULL, is = default_branch, # glue-ing happens inside check_current_branch(), where `gb` gives the # current branch - "Must be on the default branch ({ui_value(is)}), not {ui_value(gb)}." + message = c("x" = "Must be on the default branch {.val {is}}, not {.val {gb}}.") ) challenge_uncommitted_changes(msg = " There are uncommitted changes and we're about to create and push to a new \\ @@ -89,10 +89,9 @@ use_github <- function(organisation = NULL, if (is.null(organisation)) { if (visibility_specified) { - ui_stop(" - The {ui_code('visibility')} setting is only relevant for - organisation-owned repos, within the context of certain \\ - GitHub Enterprise products.") + ui_abort(" + The {.arg visibility} setting is only relevant for organisation-owned + repos, within the context of certain GitHub Enterprise products.") } visibility <- if (private) "private" else "public" } @@ -103,15 +102,15 @@ use_github <- function(organisation = NULL, whoami <- suppressMessages(gh::gh_whoami(.api_url = host)) if (is.null(whoami)) { - ui_stop(" - Unable to discover a GitHub personal access token - A token is required in order to create and push to a new repo - - Call {ui_code('gh_token_help()')} for help configuring a token") + ui_abort(c( + "x" = "Unable to discover a GitHub personal access token.", + "i" = "A token is required in order to create and push to a new repo.", + "_" = "Call {.run usethis::gh_token_help()} for help configuring a token." + )) } empirical_host <- parse_github_remotes(glue("{whoami$html_url}/REPO"))$host if (empirical_host != "github.com") { - ui_info("Targeting the GitHub host {ui_value(empirical_host)}") + ui_bullets(c("i" = "Targeting the GitHub host {.val {empirical_host}}.")) } owner <- organisation %||% whoami$login @@ -123,7 +122,9 @@ use_github <- function(organisation = NULL, repo_spec <- glue("{owner}/{repo_name}") visibility_string <- if (visibility == "public") "" else glue("{visibility} ") - ui_done("Creating {visibility_string}GitHub repository {ui_value(repo_spec)}") + ui_bullets(c( + "v" = "Creating {visibility_string}GitHub repository {.val {repo_spec}}." + )) if (is.null(organisation)) { create <- gh::gh( "POST /user/repos", @@ -153,7 +154,7 @@ use_github <- function(organisation = NULL, ) withr::defer(view_url(create$html_url)) - ui_done("Setting remote {ui_value('origin')} to {ui_value(origin_url)}") + ui_bullets(c("v" = "Setting remote {.val origin} to {.val {origin_url}}.")) use_git_remote("origin", origin_url) if (is_package()) { @@ -171,8 +172,7 @@ use_github <- function(organisation = NULL, repo <- git_repo() gbl <- gert::git_branch_list(local = TRUE, repo = repo) if (nrow(gbl) > 1) { - ui_done(" - Setting {ui_value(default_branch)} as default branch on GitHub") + ui_bullets(c("v" = "Setting {.val {default_branch}} as default branch on GitHub.")) gh::gh( "PATCH /repos/{owner}/{repo}", owner = owner, repo = repo_name, @@ -258,11 +258,12 @@ has_github_links <- function() { check_no_origin <- function() { remotes <- git_remotes() if ("origin" %in% names(remotes)) { - ui_stop(" - This repo already has an {ui_value('origin')} remote, \\ - with value {ui_value(remotes[['origin']])}. - You can remove this setting with: - {ui_code('usethis::use_git_remote(\"origin\", url = NULL, overwrite = TRUE)')}") + ui_abort(c( + "x" = "This repo already has an {.val origin} remote, with value + {.val {remotes[['origin']]}}.", + "i" = "You can remove this setting with:", + " " = '{.code usethis::use_git_remote("origin", url = NULL, overwrite = TRUE)}' + )) } invisible() } @@ -284,5 +285,5 @@ check_no_github_repo <- function(owner, repo, host) { } spec <- glue("{owner}/{repo}") empirical_host <- parse_github_remotes(repo_info$html_url)$host - ui_stop("Repo {ui_value(spec)} already exists on {ui_value(empirical_host)}") + ui_abort("Repo {.val {spec}} already exists on {.val {empirical_host}}.") } diff --git a/R/github_token.R b/R/github_token.R index bc9281fc8..f6c68bc0d 100644 --- a/R/github_token.R +++ b/R/github_token.R @@ -67,11 +67,12 @@ create_github_token <- function(scopes = c("repo", "user", "gist", "workflow"), withr::defer(view_url(url)) hint <- code_hint_with_host("gitcreds::gitcreds_set", host) - ui_todo(" - Call {ui_code(hint)} to register this token in the \\ - local Git credential store - It is also a great idea to store this token in any password-management \\ - software that you use") + ui_bullets(c( + "_" = "Call {.code {hint}} to register this token in the local Git + credential store.", + "i" = "It is also a great idea to store this token in any + password-management software that you use." + )) invisible() } @@ -118,63 +119,73 @@ pat_sitrep <- function(host = "https://github.com", maybe_pat <- purrr::safely(gh::gh_token)(api_url = host) if (is.null(maybe_pat$result)) { - ui_oops("The PAT discovered for {ui_path(host)} has the wrong structure.") - ui_inform(maybe_pat$error) + ui_bullets(c( + "x" = "The PAT discovered for {.url {host}} has the wrong structure." + )) + ui_bullets(c("i" = maybe_pat$error)) return(invisible(FALSE)) } pat <- maybe_pat$result have_pat <- pat != "" if (!have_pat) { - kv_line("Personal access token for {ui_value(host)}", NULL) + kv_line("Personal access token for {.val {host}}", NULL) hint <- code_hint_with_host("create_github_token", host, "host") - ui_todo("To create a personal access token, call {ui_code(hint)}") + ui_bullets(c( + "_" = "To create a personal access token, call {.code {hint}}." + )) hint <- code_hint_with_host("gitcreds::gitcreds_set", host) - ui_todo("To store a token for current and future use, call {ui_code(hint)}") - ui_info(" - Read more in the {ui_value('Managing Git(Hub) Credentials')} article: - https://usethis.r-lib.org/articles/articles/git-credentials.html") + url <- "https://usethis.r-lib.org/articles/articles/git-credentials.html" + ui_bullets(c( + "_" = "To store a token for current and future use, call {.code {hint}}.", + "i" = "Read more in the {.href [Managing Git(Hub) Credentials]({url})} article." + )) return(invisible(FALSE)) } - kv_line("Personal access token for {ui_value(host)}", "") + kv_line("Personal access token for {.val {host}}", ui_special("discovered")) online <- is_online(host) if (!online) { - ui_oops(" - Host is not reachable. - No further vetting of the personal access token is possible. - Try again when {ui_value(host)} can be reached.") + ui_bullets(c( + "x" = "Host is not reachable.", + " " = "No further vetting of the personal access token is possible.", + "_" = "Try again when {.val {host}} can be reached." + )) return(invisible()) } maybe_who <- purrr::safely(gh::gh_whoami)(.token = pat, .api_url = host) if (is.null(maybe_who$result)) { - message <- "Can't get user information for this token." + message <- c("x" = "Can't get user information for this token.") if (inherits(maybe_who$error, "http_error_401")) { - message <- " - Can't get user information for this token. - The token may no longer be valid or perhaps it lacks the \\ - {ui_value('user')} scope." + message <- c( + message, + "i" = "The token may no longer be valid or perhaps it lacks the + {.val user} scope." + ) } - ui_oops(message) - ui_inform(maybe_who$error) + message <- c( + message, + "i" = maybe_who$error + ) + ui_bullets(message) return(invisible(FALSE)) } who <- maybe_who$result kv_line("GitHub user", who$login) - scopes <- who$scopes - kv_line("Token scopes", who$scopes) - scopes <- strsplit(scopes, ", ")[[1]] + scopes <- strsplit(who$scopes, ", ")[[1]] + kv_line("Token scopes", scopes) scold_for_scopes(scopes) maybe_emails <- purrr::safely(gh::gh)("/user/emails", .token = pat, .api_url = host) if (is.null(maybe_emails$result)) { - ui_oops(" - Can't retrieve registered email addresses from GitHub. - Consider re-creating your PAT with the {ui_value('user')} \\ - or at least {ui_value('user:email')} scope.") + ui_bullets(c( + "x" = "Can't retrieve registered email addresses from GitHub.", + "i" = "Consider re-creating your PAT with the {.val user} (or at least + {.val user:email}) scope." + )) } else { emails <- maybe_emails$result addresses <- map_chr( @@ -187,9 +198,10 @@ pat_sitrep <- function(host = "https://github.com", ) git_user_check(user) if (!is.null(user$email) && !any(grepl(user$email, addresses))) { - ui_oops(" - Git user's email ({ui_value(user$email)}) doesn't appear to \\ - be registered with GitHub host.") + ui_bullets(c( + "x" = "Git user's email (.val {user$email}}) doesn't appear to be + registered with GitHub host." + )) } } @@ -211,29 +223,30 @@ scold_for_renviron <- function() { fishy_keys <- re_match(fishy_lines, "^(?.+)=.+")$key # TODO: when I switch to cli, this is a good place for `!` # in general, lots below is suboptimal, but good enough for now - ui_info(c( - "{ui_path(renviron_path)} defines environment variable(s):", - paste0("- ", fishy_keys), - "This can prevent your PAT from being retrieved from the Git credential store." + ui_bullets(c( + "!" = "{.path {pth(renviron_path)}} defines{cli::qty(length(fishy_keys))} + the environment variable{?s}:", + bulletize(fishy_keys), + "!" = "This can prevent your PAT from being retrieved from the Git + credential store.", + "i" = "If you are troubleshooting PAT problems, the root cause may be an + old, invalid PAT defined in {.path {pth(renviron_path)}}.", + "i" = "For most use cases, it is better to NOT define the PAT in + {.file .Renviron}.", + "_" = "Call {.fun edit_r_environ} to edit that file.", + "_" = "Then call {.run gitcreds::gitcreds_set()} to put the PAT into + the Git credential store." )) - ui_info(" - If you are troubleshooting PAT problems, the root cause may be an old, \\ - invalid PAT defined in {ui_path(renviron_path)}.") - ui_todo("Call {ui_code('edit_r_environ()')} to edit that file.") - ui_info(" - For most use cases, it is better to NOT define the PAT in \\ - {ui_code('.Renviron')}. - Instead, call {ui_code('gitcreds::gitcreds_set()')} to put the PAT into \\ - the Git credential store.") - invisible() } scold_for_scopes <- function(scopes) { if (length(scopes) == 0) { - ui_oops(" - Token has no scopes! - {ui_code('create_github_token()')} defaults to the recommended scopes.") + ui_bullets(c( + "x" = "Token has no scopes!", + "i" = "Tokens initiated with {.fun create_github_token} default to the + recommended scopes." + )) return(invisible()) } @@ -250,23 +263,17 @@ scold_for_scopes <- function(scopes) { return(invisible()) } - # current design of the ui_*() functions makes this pretty hard :( suggestions <- c( - if (!has_repo) { - "- {ui_value('repo')}: needed to fully access user's repos" - }, - if (!has_workflow) { - "- {ui_value('workflow')}: needed to manage GitHub Actions workflow files" - }, - if (!has_user_email) { - "- {ui_value('user:email')}: needed to read user's email addresses" - } + "*" = if (!has_repo) "{.val repo}: needed to fully access user's repos", + "*" = if (!has_workflow) "{.val workflow}: needed to manage GitHub Actions workflow files", + "*" = if (!has_user_email) "{.val user:email}: needed to read user's email addresses" ) message <- c( - "Token lacks recommended scopes:", + "!" = "Token lacks recommended scopes:", suggestions, - "Consider re-creating your PAT with the missing scopes.", - "{ui_code('create_github_token()')} defaults to the recommended scopes." + "i" = "Consider re-creating your PAT with the missing scopes.", + "i" = "Tokens initiated with {.fun usethis::create_github_token} default to the + recommended scopes." ) - ui_oops(glue_collapse(message, sep = "\n")) + ui_bullets(message) } diff --git a/R/helpers.R b/R/helpers.R index fb2b9803e..edbdbdd87 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -7,9 +7,9 @@ use_dependency <- function(package, type, min_version = NULL) { } if (package == "R" && tolower(type) != "depends") { - ui_stop("Set {ui_code('type = \"Depends\"')} when specifying an R version") + ui_abort('Set {.code type = "Depends"} when specifying an R version.') } else if (package == "R" && is.null(min_version)) { - ui_stop("Specify {ui_code('min_version')} when {ui_code('package = \"R\"')}") + ui_abort('Specify {.arg min_version} when {.code package = "R"}.') } if (isTRUE(min_version) && package == "R") { @@ -38,7 +38,9 @@ use_dependency <- function(package, type, min_version = NULL) { # * First use of a LinkingTo package as a non-LinkingTo dependency # In all cases, we can can simply make the change. if (nrow(deps) == 0 || new_linking_to || new_non_linking_to) { - ui_done("Adding {ui_value(package)} to {ui_field(type)} field in DESCRIPTION") + ui_bullets(c( + "v" = "Adding {.pkg {package}} to {.field {type}} field in DESCRIPTION." + )) desc$set_dep(package, type, version = version) desc$write() changed <- TRUE @@ -56,27 +58,29 @@ use_dependency <- function(package, type, min_version = NULL) { delta <- sign(match(existing_type, types) - match(type, types)) if (delta < 0) { # don't downgrade - ui_warn( - "Package {ui_value(package)} is already listed in \\ - {ui_value(existing_type)} in DESCRIPTION, no change made." - ) + ui_bullets(c( + "!" = "Package {.pkg {package}} is already listed in + {.field {existing_type}} in DESCRIPTION; no change made." + )) } else if (delta == 0 && !is.null(min_version)) { # change version upgrade <- existing_version == "*" || numeric_version(min_version) > version_spec(existing_version) if (upgrade) { - ui_done( - "Increasing {ui_value(package)} version to {ui_value(version)} in \\ - DESCRIPTION") + ui_bullets(c( + "v" = "Increasing {.pkg {package}} version to {.val {version}} in + DESCRIPTION." + )) desc$set_dep(package, type, version = version) desc$write() changed <- TRUE } } else if (delta > 0) { # moving from, e.g., Suggests to Imports - ui_done( - "Moving {ui_value(package)} from {ui_field(existing_type)} to \\ - {ui_field(type)} field in DESCRIPTION") + ui_bullets(c( + "v" = "Moving {.pkg {package}} from {.field {existing_type}} to + {.field {type}} field in DESCRIPTION." + )) desc$del_dep(package, existing_type) desc$set_dep(package, type, version = version) desc$write() @@ -99,10 +103,10 @@ version_spec <- function(x) { view_url <- function(..., open = is_interactive()) { url <- paste(..., sep = "/") if (open) { - ui_done("Opening URL {ui_value(url)}") + ui_bullets(c("v" = "Opening URL {.url {url}}.")) utils::browseURL(url) } else { - ui_todo("Open URL {ui_value(url)}") + ui_bullets(c("_" = "Open URL {.url {url}}.")) } invisible(url) } diff --git a/R/issue.R b/R/issue.R index 16181f8c9..a4b0e6085 100644 --- a/R/issue.R +++ b/R/issue.R @@ -41,23 +41,25 @@ issue_close_community <- function(number, reprex = FALSE) { # https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization#repository-access-for-each-permission-level # I have not found a way to detect triage permission via API. # It seems you just have to try? - ui_line(" - You don't seem to have push access for {ui_value(tr$repo_spec)}. - Unless you have triage permissions, you won't be allowed to close an \\ - issue.") - if (ui_nope("Do you want to try anyway?")) { - ui_oops("Cancelling.") + ui_bullets(c( + "!" = "You don't seem to have push access for {.val {tr$repo_spec}}.", + "i" = "Unless you have triage permissions, you won't be allowed to close + an issue." + )) + if (ui_nah("Do you want to try anyway?")) { + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } info <- issue_info(number, tr) issue <- issue_details(info) - ui_done(" - Closing issue {ui_value(issue$shorthand)} \\ - ({ui_field(issue$author)}): {ui_value(issue$title)}") + ui_bullets(c( + "v" = "Closing issue {.val {issue$shorthand}} ({.field {issue$author}}): + {.val {issue$title}}." + )) if (info$state == "closed") { - ui_stop("Issue {number} is already closed") + ui_abort("Issue {.val {number}} is already closed.") } reprex_insert <- glue(" @@ -88,12 +90,13 @@ issue_reprex_needed <- function(number) { # https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization#repository-access-for-each-permission-level # I can't find anyway to detect triage permission via API. # It seems you just have to try? - ui_line(" - You don't seem to have push access for {ui_value(tr$repo_spec)}. - Unless you have triage permissions, you won't be allowed to label an \\ - issue.") - if (ui_nope("Do you want to try anyway?")) { - ui_oops("Cancelling.") + ui_bullets(c( + "!" = "You don't seem to have push access for {.val {tr$repo_spec}}.", + "i" = "Unless you have triage permissions, you won't be allowed to label + an issue." + )) + if (ui_nah("Do you want to try anyway?")) { + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } @@ -102,12 +105,13 @@ issue_reprex_needed <- function(number) { labels <- map_chr(info$labels, "name") issue <- issue_details(info) if ("reprex" %in% labels) { - ui_stop("Issue {number} already has 'reprex' label") + ui_abort("Issue {.val {number}} already has {.val reprex} label.") } - ui_done(" - Labelling and commenting on issue {ui_value(issue$shorthand)} \\ - ({ui_field(issue$author)}): {ui_value(issue$title)}") + ui_bullets(c( + "v" = "Labelling and commenting on issue {.val {issue$shorthand}} + ({.field {issue$author}}): {.val {issue$title}}." + )) message <- glue(" Can you please provide a minimal reproducible example using the \\ diff --git a/R/license.R b/R/license.R index 2d668f000..0e3340eab 100644 --- a/R/license.R +++ b/R/license.R @@ -186,8 +186,7 @@ check_license_version <- function(version, possible) { version <- as.double(version) if (!version %in% possible) { - possible <- glue_collapse(possible, sep = ", ", last = ", or ") - ui_stop("`version` must be {possible}") + ui_abort("{.arg version} must be {.or {possible}}.") } version diff --git a/R/lifecycle.R b/R/lifecycle.R index f87419034..cd4f13c5a 100644 --- a/R/lifecycle.R +++ b/R/lifecycle.R @@ -18,8 +18,9 @@ use_lifecycle <- function() { check_is_package("use_lifecycle()") check_uses_roxygen("use_lifecycle()") if (!uses_roxygen_md()) { - ui_stop(" - Turn on roxygen2 markdown support {ui_code('use_roxygen_md()')}") + ui_abort(" + Turn on roxygen2 markdown support with {.run usethis::use_roxygen_md()}, + then try again.") } use_package("lifecycle") @@ -32,13 +33,12 @@ use_lifecycle <- function() { templ_files <- dir_ls(templ_dir, glob = "*/lifecycle-*.svg") purrr::walk(templ_files, file_copy, dest_dir, overwrite = TRUE) - ui_done("Copied SVG badges to {ui_path(dest_dir)}") - - ui_todo(c( - "Add badges in documentation topics by inserting one of:", - "#' `r lifecycle::badge('experimental')`", - "#' `r lifecycle::badge('superseded')`", - "#' `r lifecycle::badge('deprecated')`" + ui_bullets(c( + "v" = "Copied SVG badges to {.path {pth(dest_dir)}}.", + "_" = "Add badges in documentation topics by inserting a line like this:", + " " = "#' `r lifecycle::badge('experimental')`", + " " = "#' `r lifecycle::badge('superseded')`", + " " = "#' `r lifecycle::badge('deprecated')`" )) invisible(TRUE) diff --git a/R/logo.R b/R/logo.R index d6199f536..e5d90584e 100644 --- a/R/logo.R +++ b/R/logo.R @@ -29,7 +29,7 @@ use_logo <- function(img, geometry = "240x278", retina = TRUE) { if (path_ext(img) == "svg") { logo_path <- path("man", "figures", "logo.svg") file_copy(img, proj_path(logo_path), overwrite = TRUE) - ui_done("Copied {ui_path(img)} to {ui_path(logo_path)}") + ui_bullets(c("v" = "Copied {.path {pth(img)}} to {.path {logo_path}}.")) height <- as.integer(sub(".*x", "", geometry)) } else { @@ -38,7 +38,7 @@ use_logo <- function(img, geometry = "240x278", retina = TRUE) { img_data <- magick::image_read(img) img_data <- magick::image_resize(img_data, geometry) magick::image_write(img_data, logo_path) - ui_done("Resized {ui_path(img)} to {geometry}") + ui_bullets(c("v" = "Resized {.path {pth(img)}} to {geometry}.")) height <- magick::image_info(magick::image_read(logo_path))$height } @@ -48,11 +48,17 @@ use_logo <- function(img, geometry = "240x278", retina = TRUE) { height <- round(height / 2) } - ui_todo("Add logo to your README with the following html:") + ui_bullets(c("_" = "Add logo to your README with the following html:")) pd_link <- pkgdown_url(pedantic = TRUE) if (is.null(pd_link)) { - ui_code_block("# {pkg} \"\"") + ui_code_snippet( + "# {pkg} \"\"", + language = "" + ) } else { - ui_code_block("# {pkg} \"{pkg}") + ui_code_snippet( + "# {pkg} \"{pkg}", + language = "" + ) } } diff --git a/R/news.R b/R/news.R index cf82197e2..548377c4a 100644 --- a/R/news.R +++ b/R/news.R @@ -50,10 +50,10 @@ use_news_heading <- function(version) { if (development_title == news[[1]]) { news[[1]] <- title - ui_done("Replacing development heading in NEWS.md") + ui_bullets(c("v" = "Replacing development heading in {.path NEWS.md}.")) return(write_utf8(news_path, news)) } - ui_done("Adding new heading to NEWS.md") + ui_bullets(c("v" = "Adding new heading to {.path NEWS.md}.")) write_utf8(news_path, c(title, "", news)) } diff --git a/R/package.R b/R/package.R index 910d99f0e..3c8ac4289 100644 --- a/R/package.R +++ b/R/package.R @@ -83,9 +83,10 @@ use_remote <- function(package, package_remote = NULL) { package_remote <- package_remote(package_desc) } - ui_done(" - Adding {ui_value(package_remote)} to {ui_field('Remotes')} field in \\ - DESCRIPTION") + ui_bullets(c( + "v" = "Adding {.val {package_remote}} to {.field Remotes} field in + DESCRIPTION." + )) remotes <- c(remotes, package_remote) desc$set_remotes(remotes) @@ -113,32 +114,34 @@ package_remote <- function(desc) { urls <- desc_urls(package, desc = desc) urls <- urls[urls$is_github, ] if (nrow(urls) < 1) { - ui_stop("Cannot determine remote for {ui_value(package)}") + ui_abort("Cannot determine remote for {.pkg {package}}.") } parsed <- parse_github_remotes(urls$url[[1]]) remote <- paste0(parsed$repo_owner, "/", parsed$repo_name) - if (ui_yeah(" - {ui_value(package)} was either installed from CRAN or local source. - Based on DESCRIPTION, we propose the remote: {ui_value(remote)} - Is this OK?")) { + if (ui_yep(c( + "!" = "{.pkg {package}} was either installed from CRAN or local source.", + "i" = "Based on DESCRIPTION, we propose the remote: {.val {remote}}.", + " " = "Is this OK?" + ))) { remote } else { - ui_stop("Cannot determine remote for {ui_value(package)}") + ui_abort("Cannot determine remote for {.pkg {package}}.") } } refuse_package <- function(package, verboten) { if (package %in% verboten) { - code <- glue("use_package(\"{package}\", type = \"depends\")") - ui_stop( - "{ui_value(package)} is a meta-package and it is rarely a good idea to \\ - depend on it. Please determine the specific underlying package(s) that \\ - offer the function(s) you need and depend on that instead. \\ - For data analysis projects that use a package structure but do not implement \\ - a formal R package, adding {ui_value(package)} to Depends is a \\ - reasonable compromise. Call {ui_code(code)} to achieve this. - " - ) + code <- glue('use_package("{package}", type = "depends")') + ui_abort(c( + "x" = "{.pkg {package}} is a meta-package and it is rarely a good idea to + depend on it.", + "_" = "Please determine the specific underlying package(s) that provide + the function(s) you need and depend on that instead.", + "i" = "For data analysis projects that use a package structure but do not + implement a formal R package, adding {.pkg {package}} to + {.field Depends} is a reasonable compromise.", + "_" = "Call {.code {code}} to achieve this." + )) } invisible(package) } @@ -151,11 +154,13 @@ how_to_use <- function(package, type) { } switch(type, - imports = ui_todo("Refer to functions with {ui_code(paste0(package, '::fun()'))}"), - depends = ui_todo( - "Are you sure you want {ui_field('Depends')}? \\ - {ui_field('Imports')} is almost always the better choice." - ), + imports = ui_bullets(c( + "_" = "Refer to functions with {.code {paste0(package, '::fun()')}}." + )), + depends = ui_bullets(c( + "!" = "Are you sure you want {.field Depends}? + {.field Imports} is almost always the better choice." + )), suggests = suggests_usage_hint(package), enhances = "", linkingto = show_includes(package) @@ -167,16 +172,17 @@ suggests_usage_hint <- function(package) { if (imports_rlang) { code1 <- glue('rlang::is_installed("{package}")') code2 <- glue('rlang::check_installed("{package}")') - ui_todo(" - In your package code, use {ui_code(code1)} or {ui_code(code2)} to test \\ - if {package} is installed") + ui_bullets(c( + "_" = "In your package code, use {.code {code1}} or {.code {code2}} to + test if {.pkg {package}} is installed." + )) code <- glue("{package}::fun()") - ui_todo("Then directly refer to functions with {ui_code(code)}") + ui_bullets(c("_" = "Then directly refer to functions with {.code {code}}.")) } else { - code <- glue("requireNamespace(\"{package}\", quietly = TRUE)") - ui_todo("Use {ui_code(code)} to test if package is installed") + code <- glue('requireNamespace("{package}", quietly = TRUE)') + ui_bullets(c("_" = "Use {.code {code}} to test if package is installed.")) code <- glue("{package}::fun()") - ui_todo("Then directly refer to functions with {ui_code(code)}") + ui_bullets(c("_" = "Then directly refer to functions with {.code {code}}.")) } } @@ -187,6 +193,6 @@ show_includes <- function(package) { return() } - ui_todo("Possible includes are:") - ui_code_block("#include <{path_file(h)}>", copy = FALSE) + ui_bullets(c("Possible includes are:")) + ui_code_snippet("#include <{path_file(h)}>", copy = FALSE, language = "") } diff --git a/R/pkgdown.R b/R/pkgdown.R index 534b05f1c..78b3457d0 100644 --- a/R/pkgdown.R +++ b/R/pkgdown.R @@ -86,9 +86,10 @@ use_pkgdown_url <- function(url, tr = NULL) { tr <- tr %||% target_repo(github_get = TRUE) config_path <- pkgdown_config_path() - ui_done(" - Recording {ui_value(url)} as site's {ui_field('url')} in \\ - {ui_path(config_path)}") + ui_bullets(c( + "v" = "Recording {.url {url}} as site's {.field url} in + {.path {pth(config_path)}}." + )) config <- pkgdown_config_meta() if (has_name(config, "url")) { config$url <- url @@ -99,15 +100,17 @@ use_pkgdown_url <- function(url, tr = NULL) { proj_desc_field_update("URL", url, append = TRUE) if (has_package_doc()) { - ui_todo(" - Run {ui_code('devtools::document()')} to update package-level documentation.") + ui_bullets(c( + "_" = "Run {.run devtools::document()} to update package-level documentation." + )) } gh <- gh_tr(tr) homepage <- gh("GET /repos/{owner}/{repo}")[["homepage"]] if (is.null(homepage) || homepage != url) { - ui_done("Setting {ui_value(url)} as homepage of GitHub repo \\ - {ui_value(tr$repo_spec)}") + ui_bullets(c( + "v" = "Setting {.url {url}} as homepage of GitHub repo {.val {tr$repo_spec}}." + )) gh("PATCH /repos/{owner}/{repo}", homepage = url) } @@ -125,11 +128,11 @@ tidyverse_url <- function(url, tr = NULL) { if (grepl(glue("{custom_url}/?"), url)) { return(url) } - if (ui_yeah(" - {ui_value(tr$repo_name)} is owned by the {ui_value(tr$repo_owner)} GitHub \\ - organization. - Shall we configure {ui_value(custom_url)} as the (eventual) \\ - pkgdown URL?")) { + if (ui_yep(c( + "i" = "{.val {tr$repo_name}} is owned by the {.val {tr$repo_owner}} GitHub + organization.", + " " = "Shall we configure {.val {custom_url}} as the (eventual) pkgdown URL?" + ))) { custom_url } else { url @@ -175,9 +178,10 @@ pkgdown_url <- function(pedantic = FALSE) { } if (pedantic) { - ui_warn(" - pkgdown config does not specify the site's {ui_field('url')}, \\ - which is optional but recommended") + ui_bullets(c( + "!" = "{.pkg pkgdown} config does not specify the site's {.field url}, + which is optional but recommended." + )) } NULL } diff --git a/R/pr.R b/R/pr.R index 26de04689..abb496502 100644 --- a/R/pr.R +++ b/R/pr.R @@ -155,9 +155,10 @@ pr_init <- function(branch) { repo <- git_repo() if (gert::git_branch_exists(branch, local = TRUE, repo = repo)) { - code <- glue("pr_resume(\"{branch}\")") - ui_info(" - Branch {ui_value(branch)} already exists, calling {ui_code(code)}") + code <- glue('pr_resume("{branch}")') + ui_bullets(c( + "i" = "Branch {.val {branch}} already exists, calling {.code {code}}." + )) return(pr_resume(branch)) } @@ -169,14 +170,16 @@ pr_init <- function(branch) { maybe_good_configs <- c("maybe_ours_or_theirs", "maybe_fork") if (cfg$type %in% maybe_good_configs) { - ui_line(' - Unable to confirm the GitHub remote configuration is "pull request ready". - You probably need to configure a personal access token for \\ - {ui_value(tr$host)}. - See {ui_code("gh_token_help()")} for help. - (Or maybe we\'re just offline?)') + ui_bullets(c( + "x" = 'Unable to confirm the GitHub remote configuration is + "pull request ready".', + "i" = "You probably need to configure a personal access token for + {.val {tr$host}}.", + "i" = "See {.run usethis::gh_token_help()} for help with that.", + "i" = "(Or maybe we're just offline?)" + )) if (ui_github_remote_config_wat(cfg)) { - ui_oops("Cancelling.") + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } @@ -207,20 +210,21 @@ pr_init <- function(branch) { if (comparison$remote_only > 0) { challenge_uncommitted_changes() } - ui_done("Pulling changes from {ui_value(remref)}.") + ui_bullets(c("v" = "Pulling changes from {.val {remref}}.")) git_pull(remref = remref, verbose = FALSE) } } else { - ui_info(" - Unable to pull changes for current branch, since we are offline.") + ui_bullets(c( + "!" = "Unable to pull changes for current branch, since we are offline." + )) } - ui_done("Creating and switching to local branch {ui_value(branch)}.") + ui_bullets(c("v" = "Creating and switching to local branch {.val {branch}}.")) gert::git_branch_create(branch, repo = repo) config_key <- glue("branch.{branch}.created-by") gert::git_config_set(config_key, value = "usethis::pr_init", repo = repo) - ui_todo("Use {ui_code('pr_push()')} to create a PR.") + ui_bullets(c("_" = "Use {.fun pr_push} to create a PR.")) invisible() } @@ -230,35 +234,37 @@ pr_resume <- function(branch = NULL) { repo <- git_repo() if (is.null(branch)) { - ui_info(" - No branch specified ... looking up local branches and associated PRs.") + ui_bullets(c( + "i" = "No branch specified ... looking up local branches and associated PRs." + )) default_branch <- git_default_branch() branch <- choose_branch(exclude = default_branch) if (is.null(branch)) { - ui_oops("Repo doesn't seem to have any non-default branches.") + ui_bullets(c("x" = "Repo doesn't seem to have any non-default branches.")) return(invisible()) } if (length(branch) == 0) { - ui_oops("No branch selected, exiting.") + ui_bullets(c("x" = "No branch selected, exiting.")) return(invisible()) } } check_string(branch) if (!gert::git_branch_exists(branch, local = TRUE, repo = repo)) { - code <- glue("pr_init(\"{branch}\")") - ui_stop(" - No branch named {ui_value(branch)} exists. - Call {ui_code(code)} to create a new PR branch.") + code <- glue('pr_init("{branch}")') + ui_abort(c( + "x" = "No branch named {.val {branch}} exists.", + "_" = "Call {.code {code}} to create a new PR branch." + )) } challenge_uncommitted_changes() - ui_done("Switching to branch {ui_value(branch)}.") + ui_bullets(c("v" = "Switching to branch {.val {branch}}.")) gert::git_branch_checkout(branch, repo = repo) git_pull() - ui_todo("Use {ui_code('pr_push()')} to create or update PR.") + ui_bullets(c("_" = "Use {.fun usethis::pr_push} to create or update PR.")) invisible() } @@ -280,14 +286,14 @@ pr_fetch <- function(number = NULL, target = c("source", "primary")) { challenge_uncommitted_changes() if (is.null(number)) { - ui_info("No PR specified ... looking up open PRs.") + ui_bullets(c("i" = "No PR specified ... looking up open PRs.")) pr <- choose_pr(tr = tr) if (is.null(pr)) { - ui_oops("No open PRs found for {ui_value(tr$repo_spec)}.") + ui_bullets(c("x" = "No open PRs found for {.val {tr$repo_spec}}.")) return(invisible()) } if (min(lengths(pr)) == 0) { - ui_oops("No PR selected, exiting.") + ui_bullets(c("x" = "No PR selected, exiting.")) return(invisible()) } } else { @@ -295,26 +301,28 @@ pr_fetch <- function(number = NULL, target = c("source", "primary")) { } if (is.na(pr$pr_repo_owner)) { - ui_stop(" - The repo or branch where PR #{pr$pr_number} originates seems to have been \\ - deleted.") + ui_abort(" + The repo or branch where PR #{pr$pr_number} originates seems to have been + deleted.") } pr_user <- glue("@{pr$pr_user}") - ui_done(" - Checking out PR {ui_value(pr$pr_string)} ({ui_field(pr_user)}): \\ - {ui_value(pr$pr_title)}.") + ui_bullets(c( + "v" = "Checking out PR {.val {pr$pr_string}} ({.field {pr_user}}): + {.val {pr$pr_title}}." + )) if (pr$pr_from_fork && isFALSE(pr$maintainer_can_modify)) { - ui_info(" - Note that user does NOT allow maintainer to modify this PR at this \\ - time, although this can be changed.") + ui_bullets(c( + "!" = "Note that user does NOT allow maintainer to modify this PR at this + time, although this can be changed." + )) } remote <- github_remote_list(pr$pr_remote) if (nrow(remote) == 0) { url <- switch(tr$protocol, https = pr$pr_https_url, ssh = pr$pr_ssh_url) - ui_done("Adding remote {ui_value(pr$pr_remote)} as {ui_value(url)}.") + ui_bullets(c("v" = "Adding remote {.val {pr$pr_remote}} as {.val {url}}.")) gert::git_remote_add(url = url, name = pr$pr_remote, repo = repo) config_key <- glue("remote.{pr$pr_remote}.created-by") gert::git_config_set(config_key, "usethis::pr_fetch", repo = repo) @@ -334,9 +342,10 @@ pr_fetch <- function(number = NULL, target = c("source", "primary")) { # Create local branch, if necessary, and switch to it ---- if (!gert::git_branch_exists(pr$pr_local_branch, local = TRUE, repo = repo)) { - ui_done(" - Creating and switching to local branch {ui_value(pr$pr_local_branch)}.") - ui_done("Setting {ui_value(pr_remref)} as remote tracking branch.") + ui_bullets(c( + "v" = "Creating and switching to local branch {.val {pr$pr_local_branch}}.", + "v" = "Setting {.val {pr_remref}} as remote tracking branch." + )) gert::git_branch_create(pr$pr_local_branch, ref = pr_remref, repo = repo) config_key <- glue("branch.{pr$pr_local_branch}.created-by") gert::git_config_set(config_key, "usethis::pr_fetch", repo = repo) @@ -346,7 +355,7 @@ pr_fetch <- function(number = NULL, target = c("source", "primary")) { } # Local branch pre-existed; make sure tracking branch is set, switch, & pull - ui_done("Switching to branch {ui_value(pr$pr_local_branch)}.") + ui_bullets(c("v" = "Switching to branch {.val {pr$pr_local_branch}}.")) gert::git_branch_checkout(pr$pr_local_branch, repo = repo) config_url <- glue("branch.{pr$pr_local_branch}.pr-url") gert::git_config_set(config_url, pr$pr_html_url, repo = repo) @@ -354,7 +363,7 @@ pr_fetch <- function(number = NULL, target = c("source", "primary")) { pr_branch_ours_tracking <- git_branch_tracking(pr$pr_local_branch) if (is.na(pr_branch_ours_tracking) || pr_branch_ours_tracking != pr_remref) { - ui_done("Setting {ui_value(pr_remref)} as remote tracking branch.") + ui_bullets(c("v" = "Setting {.val {pr_remref}} as remote tracking branch.")) gert::git_branch_set_upstream(pr_remref, repo = repo) } git_pull(verbose = FALSE) @@ -376,13 +385,14 @@ pr_push <- function() { # this is the first push if (cfg$type == "fork" && cfg$upstream$can_push && is_interactive()) { choices <- c( - origin = glue( - "{cfg$origin$repo_spec} = {ui_value('origin')} (external PR)"), - upstream = glue( - "{cfg$upstream$repo_spec} = {ui_value('upstream')} (internal PR)") + origin = ui_pre_glue(" + <> = {.val origin} (external PR)"), + upstream = ui_pre_glue(" + <> = {.val upstream} (internal PR)") ) + choices_formatted <- map_chr(choices, cli::format_inline) title <- glue("Which repo do you want to push to?") - choice <- utils::menu(choices, graphics = FALSE, title = title) + choice <- utils::menu(choices_formatted, graphics = FALSE, title = title) remote <- names(choices)[[choice]] } else { remote <- "origin" @@ -400,8 +410,9 @@ pr_push <- function() { if (is.null(pr)) { pr_create() } else { - ui_todo(" - View PR at {ui_value(pr$pr_html_url)} or call {ui_code('pr_view()')}.") + ui_bullets(c( + "_" = "View PR at {.url {pr$pr_html_url}} or call {.run usethis::pr_view()}." + )) } invisible() @@ -431,7 +442,7 @@ pr_merge_main <- function() { tr <- target_repo(github_get = TRUE, ask = FALSE) challenge_uncommitted_changes() remref <- glue("{tr$remote}/{tr$default_branch}") - ui_done("Pulling changes from {ui_value(remref)}.") + ui_bullets(c("v" = "Pulling changes from {.val {remref}}.")) git_pull(remref, verbose = FALSE) } @@ -446,13 +457,15 @@ pr_view <- function(number = NULL, target = c("source", "primary")) { if (branch != default_branch) { url <- pr_url(branch = branch, tr = tr) if (is.null(url)) { - ui_info(" - Current branch ({ui_value(branch)}) does not appear to be \\ - connected to a PR.") + ui_bullets(c( + "i" = "Current branch ({.val {branch}}) does not appear to be + connected to a PR." + )) } else { number <- sub("^.+pull/", "", url) - ui_info(" - Current branch ({ui_value(branch)}) is connected to PR #{number}.") + ui_bullets(c( + "i" = "Current branch ({.val {branch}}) is connected to PR #{number}." + )) } } } else { @@ -460,14 +473,14 @@ pr_view <- function(number = NULL, target = c("source", "primary")) { url <- pr$pr_html_url } if (is.null(url)) { - ui_info("No PR specified ... looking up open PRs.") + ui_bullets(c("i" = "No PR specified ... looking up open PRs.")) pr <- choose_pr(tr = tr) if (is.null(pr)) { - ui_oops("No open PRs found for {ui_value(tr$repo_spec)}.") + ui_bullets(c("x" = "No open PRs found for {.val {tr$repo_spec}}.")) return(invisible()) } if (min(lengths(pr)) == 0) { - ui_oops("No PR selected, exiting.") + ui_bullets(c("x" = "No PR selected, exiting.")) return(invisible()) } url <- pr$pr_html_url @@ -481,19 +494,22 @@ pr_pause <- function() { # intentionally naive selection of target repo tr <- target_repo(github_get = FALSE, ask = FALSE) - ui_done("Switching back to the default branch.") + ui_bullets(c("v" = "Switching back to the default branch.")) default_branch <- git_default_branch() if (git_branch() == default_branch) { - ui_info(" - Already on this repo's default branch ({ui_value(default_branch)}), \\ - nothing to do.") + ui_bullets(c( + "!" = "Already on this repo's default branch ({.val {default_branch}}), + nothing to do." + )) return(invisible()) } challenge_uncommitted_changes() # TODO: what happens here if offline? check_branch_pulled(use = "pr_pull()") - ui_done("Switching back to default branch ({ui_value(default_branch)}).") + ui_bullets(c( + "v" = "Switching back to default branch ({.val {default_branch}})." + )) gert::git_branch_checkout(default_branch, repo = git_repo()) pr_pull_source_override(tr = tr, default_branch = default_branch) } @@ -542,13 +558,14 @@ pr_clean <- function(number = NULL, } tracking_branch <- git_branch_tracking(pr_local_branch) if (is.na(tracking_branch)) { - if (ui_nope(" - Local branch {ui_value(pr_local_branch)} has no associated remote \\ - branch. - If we delete {ui_value(pr_local_branch)}, any work that exists only \\ - on this branch may be hard for you to recover. - Proceed anyway?")) { - ui_oops("Cancelling.") + if (ui_nah(c( + "!" = "Local branch {.val {pr_local_branch}} has no associated remote + branch.", + "i" = "If we delete {.val {pr_local_branch}}, any work that exists only + on this branch may be hard for you to recover.", + " " = "Proceed anyway?" + ))) { + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } else { @@ -556,26 +573,31 @@ pr_clean <- function(number = NULL, branch = pr_local_branch, remref = tracking_branch ) - if (cmp$local_only > 0 && ui_nope(" - Local branch {ui_value(pr_local_branch)} has 1 or more commits \\ - that have not been pushed to {ui_value(tracking_branch)}. - If we delete {ui_value(pr_local_branch)}, this work may be hard \\ - for you to recover. - Proceed anyway?")) { - ui_oops("Cancelling.") + if (cmp$local_only > 0 && ui_nah(c( + "!" = "Local branch {.val {pr_local_branch}} has 1 or more commits that + have not been pushed to {.val {tracking_branch}}.", + "i" = "If we delete {.val {pr_local_branch}}, this work may be hard for + you to recover.", + " " = "Proceed anyway?" + ))) { + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } } if (git_branch() != default_branch) { - ui_done("Switching back to default branch ({ui_value(default_branch)}).") + ui_bullets(c( + "v" = "Switching back to default branch ({.val {default_branch}})." + )) gert::git_branch_checkout(default_branch, force = TRUE, repo = repo) pr_pull_source_override(tr = tr, default_branch = default_branch) } if (!is.na(pr_local_branch)) { - ui_done("Deleting local {ui_value(pr_local_branch)} branch.") + ui_bullets(c( + "v" = "Deleting local {.val {pr_local_branch}} branch." + )) gert::git_branch_delete(pr_local_branch, repo = repo) } @@ -596,7 +618,7 @@ pr_clean <- function(number = NULL, branches <- gert::git_branch_list(local = TRUE, repo = repo) branches <- branches[!is.na(branches$upstream), ] if (sum(grepl(glue("^refs/remotes/{pr$pr_remote}"), branches$upstream)) == 0) { - ui_done("Removing remote {ui_value(pr$pr_remote)}") + ui_bullets(c("v" = "Removing remote {.val {pr$pr_remote}}.")) gert::git_remote_remove(remote = pr$pr_remote, repo = repo) } invisible() @@ -615,9 +637,9 @@ pr_pull_source_override <- function(tr = NULL, default_branch = NULL) { current_branch <- git_branch() default_branch <- default_branch %||% git_default_branch() if (current_branch != default_branch) { - ui_stop(" - Internal error: pr_pull_source_override() should only be used when on \\ - default branch") + ui_abort(" + Internal error: {.fun pr_pull_source_override} should only be used when on + default branch.") } # guard against mis-configured forks, that have default branch tracking @@ -625,12 +647,13 @@ pr_pull_source_override <- function(tr = NULL, default_branch = NULL) { # TODO: should I just change the upstream tracking branch, i.e. fix it? remref <- glue("{tr$remote}/{default_branch}") if (is_online(tr$host)) { - ui_done("Pulling changes from {ui_value(remref)}") + ui_bullets(c("v" = "Pulling changes from {.val {remref}}.")) git_pull(remref = remref, verbose = FALSE) } else { - ui_info(" - Can't reach {ui_value(tr$host)}, therefore unable to pull changes from \\ - {ui_value(remref)}") + ui_bullets(c( + "!" = "Can't reach {.val {tr$host}}, therefore unable to pull changes from + {.val {remref}}." + )) } } @@ -639,7 +662,7 @@ pr_create <- function() { tracking_branch <- git_branch_tracking(branch) remote <- remref_remote(tracking_branch) remote_dat <- github_remotes(remote, github_get = FALSE) - ui_todo("Create PR at link given below") + ui_bullets(c("_" = "Create PR at link given below.")) view_url(glue_data(remote_dat, "{host_url}/{repo_spec}/compare/{branch}")) } @@ -672,12 +695,12 @@ pr_find <- function(branch = git_branch(), } if (nrow(pr_dat) > 1) { spec <- sub(":", "/", pr_head) - ui_info("Multiple PRs are associated with {ui_value(spec)}.") + ui_bullets(c("!" = "Multiple PRs are associated with {.val {spec}}.")) pr_dat <- choose_pr(pr_dat = pr_dat) if (min(lengths(pr_dat)) == 0) { - ui_stop(" + ui_abort(" One of these PRs must be specified explicitly or interactively: \\ - {ui_value(paste0('#', pr_dat$pr_number))}") + {.or {paste0('#', pr_dat$pr_number)}}.") } } @@ -762,7 +785,7 @@ pr_list <- function(tr = NULL, if (is.null(out$error)) { prs <- out$result } else { - ui_oops("Unable to retrieve PRs for {ui_value(tr$repo_spec)}.") + ui_bullets(c("x" = "Unable to retrieve PRs for {.value {tr$repo_spec}}.")) prs <- NULL } no_prs <- length(prs) == 0 @@ -832,27 +855,30 @@ choose_branch <- function(exclude = character()) { return() } prompt <- "Which branch do you want to checkout? (0 to exit)" - if (nrow(dat) > 9) { - branches_not_shown <- utils::tail(dat$name, -9) - n <- length(branches_not_shown) - dat <- dat[1:9, ] - pre <- glue("{n} branch{if (n > 1) 'es' else ''} not listed: ") - listing <- glue_collapse( - branches_not_shown, sep = ", ", width = getOption("width") - nchar(pre) + n_show_max <- 9 + n <- nrow(dat) + n_shown <- compute_n_show(n, n_show_nominal = n_show_max) + n_not_shown <- n - n_shown + if (n_not_shown > 0) { + branches_not_shown <- utils::tail(dat$name, -n_shown) + dat <- dat[seq_len(n_shown), ] + fine_print <- cli::format_inline( + "{n_not_shown} branch{?/es} not listed: {.val {branches_not_shown}}" ) - prompt <- glue(" - {prompt} - {pre}{listing}") + prompt <- glue("{prompt}\n{fine_print}") } - dat$pretty_user <- map(dat$pr_user, ~ glue("@{.x}")) + dat$pretty_user <- glue_data(dat, "@{pr_user}") dat$pretty_name <- format(dat$name, justify = "right") dat_pretty <- purrr::pmap_chr( dat[c("pretty_name", "pr_number", "pretty_user", "pr_title")], function(pretty_name, pr_number, pretty_user, pr_title) { if (is.na(pr_number)) { - glue("{pretty_name}") + pretty_name } else { - glue("{pretty_name} --> #{pr_number} ({ui_value(pretty_user)}): {pr_title}") + template <- ui_pre_glue( + "{pretty_name} {cli::symbol$arrow_right} #{pr_number} ({.field <>}): {pr_title}" + ) + cli::format_inline(template) } } ) @@ -875,13 +901,17 @@ choose_pr <- function(tr = NULL, pr_dat = NULL) { # wording needs to make sense for several PR-choosing tasks, e.g. fetch, view, # finish, forget prompt <- "Which PR are you interested in? (0 to exit)" - if (nrow(pr_dat) > 9) { - n <- nrow(pr_dat) - 9 - pr_dat <- pr_dat[1:9, ] - prompt <- glue(" - {prompt} - Not shown: {n} more {if (n > 1) 'PRs' else 'PR'}; \\ - call {ui_code('browse_github_pulls()')} to browse all PRs.") + n_show_max <- 9 + n <- nrow(pr_dat) + n_shown <- compute_n_show(n, n_show_nominal = n_show_max) + n_not_shown <- n - n_shown + if (n_not_shown > 0) { + pr_dat <- pr_dat[seq_len(n_shown), ] + info1 <- cli::format_inline("Not shown: {n_not_shown} more PR{?s}.") + info2 <- cli::format_inline( + "Call {.run usethis::browse_github_pulls()} to browse all PRs." + ) + prompt <- glue("{prompt}\n{info1}\n{info2}") } some_closed <- any(pr_dat$pr_state == "closed") @@ -891,9 +921,15 @@ choose_pr <- function(tr = NULL, pr_dat = NULL) { hash_number <- glue("#{pr_number}") at_user <- glue("@{pr_user}") if (some_closed) { - glue("{hash_number} ({ui_field(at_user)}, {pr_state}): {ui_value(pr_title)}") + template <- ui_pre_glue( + "{hash_number} ({.field <>}, {pr_state}): {.val <>}" + ) + cli::format_inline(template) } else { - glue("{hash_number} ({ui_field(at_user)}): {ui_value(pr_title)}") + template <- ui_pre_glue( + "{hash_number} ({.field <>}): {.val <>}" + ) + cli::format_inline(template) } } ) @@ -929,22 +965,25 @@ pr_branch_delete <- function(pr) { pr_remref <- glue_data(pr, "{pr_remote}/{pr_ref}") if (is.null(pr_ref)) { - ui_info(" - PR {ui_value(pr$pr_string)} originated from branch \\ - {ui_value(pr_remref)}, which no longer exists") + ui_bullets(c( + "i" = "PR {.val {pr$pr_string}} originated from branch {.val {pr_remref}}, + which no longer exists." + )) return(invisible(FALSE)) } if (is.na(pr$pr_merged_at)) { - ui_info(" - PR {ui_value(pr$pr_string)} is unmerged, \\ - we will not delete the remote branch {ui_value(pr_remref)}") + ui_bullets(c( + "i" = "PR {.val {pr$pr_string}} is unmerged, we will not delete the + remote branch {.val {pr_remref}}." + )) return(invisible(FALSE)) } - ui_done(" - PR {ui_value(pr$pr_string)} has been merged, \\ - deleting remote branch {ui_value(pr_remref)}") + ui_bullets(c( + "v" = "PR {.val {pr$pr_string}} has been merged, deleting remote branch + {.val {pr_remref}}." + )) # TODO: tryCatch here? gh( "DELETE /repos/{owner}/{repo}/git/refs/{ref}", @@ -958,11 +997,12 @@ check_pr_branch <- function(default_branch = git_default_branch()) { # current git branch check_current_branch( is_not = default_branch, - message = " - The {ui_code('pr_*()')} functions facilitate pull requests. - The current branch ({ui_value(gb)}) is this repo's default \\ - branch, but pull requests should NOT come from the default branch. - Do you need to call {ui_code('pr_init()')} (new PR)? - Or {ui_code('pr_resume()')} or {ui_code('pr_fetch()')} (existing PR)?" + message = c( + "i" = "The {.code pr_*()} functions facilitate pull requests.", + "i" = "The current branch ({.val {gb}}) is this repo's default branch, but + pull requests should NOT come from the default branch.", + "i" = "Do you need to call {.fun pr_init} (new PR)? Or {.fun pr_resume} or + {.fun pr_fetch} (existing PR)?" + ) ) } diff --git a/R/proj-desc.R b/R/proj-desc.R index b5097ff89..d2e110a7e 100644 --- a/R/proj-desc.R +++ b/R/proj-desc.R @@ -42,13 +42,12 @@ proj_desc_field_update <- function(key, value, overwrite = TRUE, append = FALSE) } if (!overwrite && length(old > 0) && any(old != "")) { - ui_stop( - "{ui_field(key)} has a different value in DESCRIPTION. \\ - Use {ui_code('overwrite = TRUE')} to overwrite." - ) + ui_abort(" + {.field {key}} has a different value in DESCRIPTION. + Use {.code overwrite = TRUE} to overwrite.") } - ui_done("Adding {ui_value(value)} to {ui_field(key)}") + ui_bullets(c("v" = "Adding {.val {value}} to {.field {key}}.")) if (append) { value <- union(old, value) diff --git a/R/proj.R b/R/proj.R index 60cd28a2c..261373efe 100644 --- a/R/proj.R +++ b/R/proj.R @@ -81,16 +81,17 @@ proj_set <- function(path = ".", force = FALSE) { path <- proj_path_prep(path) if (is.null(path) || force) { proj_string <- if (is.null(path)) "" else path - ui_done("Setting active project to {ui_value(proj_string)}") + ui_bullets(c("v" = "Setting active project to {.val {proj_string}}.")) return(proj_set_(path)) } check_path_is_directory(path) new_project <- proj_find(path) if (is.null(new_project)) { - ui_stop(' - Path {ui_path(path)} does not appear to be inside a project or package. - Read more in the help for {ui_code("proj_get()")}.') + ui_abort(c( + "Path {.path {pth(path)}} does not appear to be inside a project or package.", + "Read more in the help for {.help usethis::proj_get}." + )) } proj_set(path = new_project, force = TRUE) } @@ -103,7 +104,7 @@ proj_path <- function(..., ext = "") { has_absolute_path <- function(x) any(is_absolute_path(x)) dots <- list(...) if (any(map_lgl(dots, has_absolute_path))) { - ui_stop("Paths must be relative to the active project") + ui_abort("Paths must be relative to the active project, not absolute.") } path_norm(path(proj_get(), ..., ext = ext)) @@ -211,21 +212,22 @@ check_is_package <- function(whos_asking = NULL) { return(invisible()) } - message <- "Project {ui_value(project_name())} is not an R package." + message <- "Project {.val {project_name()}} is not an R package." if (!is.null(whos_asking)) { message <- c( - "{ui_code(whos_asking)} is designed to work with packages.", - message + "i" = "{.code {whos_asking}} is designed to work with packages.", + "x" = message ) } - ui_stop(message) + ui_abort(message) } check_is_project <- function() { if (!possibly_in_proj()) { - ui_stop(' - We do not appear to be inside a valid project or package. - Read more in the help for {ui_code("proj_get()")}.') + ui_abort(c( + "We do not appear to be inside a valid project or package.", + "Read more in the help for {.help usethis::proj_get}." + )) } } @@ -272,14 +274,18 @@ proj_activate <- function(path) { path <- user_path_prep(path) if (rstudio_available() && rstudioapi::hasFun("openProject")) { - ui_done("Opening {ui_path(path, base = NA)} in new RStudio session") + ui_bullets(c( + "v" = "Opening {.path {pth(path, base = NA)}} in new RStudio session." + )) rstudioapi::openProject(path, newSession = TRUE) invisible(FALSE) } else { proj_set(path) rel_path <- path_rel(proj_get(), path_wd()) if (rel_path != ".") { - ui_done("Changing working directory to {ui_path(path, base = NA)}") + ui_bullets(c( + "v" = "Changing working directory to {.path {pth(path, base = NA)}}" + )) setwd(proj_get()) } invisible(TRUE) diff --git a/R/rcpp.R b/R/rcpp.R index 6229f8308..0d4df066a 100644 --- a/R/rcpp.R +++ b/R/rcpp.R @@ -92,13 +92,18 @@ use_makevars <- function(settings = NULL) { if (!file_exists(makevars_path) && !file_exists(makevars_win_path)) { write_utf8(makevars_path, makevars_content) file_copy(makevars_path, makevars_win_path) - ui_done("Created {ui_path(makevars_path)} and {ui_path(makevars_win_path)} \\ - with requested compilation settings.") + ui_bullets(c( + "v" = "Created {.path {pth(makevars_path)}} and + {.path {pth(makevars_win_path)}} with requested compilation settings." + )) } else { - ui_todo("Ensure the following Makevars compilation settings are set for both \\ - {ui_path(makevars_path)} and {ui_path(makevars_win_path)}:") - ui_code_block( - makevars_content + ui_bullets(c( + "_" = "Ensure the following Makevars compilation settings are set for both + {.path {pth(makevars_path)}} and {.path {pth(makevars_win_path)}}:" + )) + ui_code_snippet( + makevars_content, + language = "" ) edit_file(makevars_path) edit_file(makevars_win_path) diff --git a/R/readme.R b/R/readme.R index 3563e0a7a..cf1a3651c 100644 --- a/R/readme.R +++ b/R/readme.R @@ -60,8 +60,9 @@ use_readme_rmd <- function(open = rlang::is_interactive()) { } if (is_pkg && !data$on_github) { - ui_todo(" - Update {ui_path('README.Rmd')} to include installation instructions.") + ui_bullets(c( + "_" = "Update {.path {pth('README.Rmd')}} to include installation instructions." + )) } if (uses_git()) { @@ -96,8 +97,9 @@ use_readme_md <- function(open = rlang::is_interactive()) { ) if (is_pkg && !data$on_github) { - ui_todo(" - Update {ui_path('README.md')} to include installation instructions.") + ui_bullets(c( + "_" = "Update {.path {pth('README.md')}} to include installation instructions." + )) } invisible(new) diff --git a/R/release.R b/R/release.R index 6114dde9b..02a122594 100644 --- a/R/release.R +++ b/R/release.R @@ -36,11 +36,12 @@ use_release_issue <- function(version = NULL) { check_is_package("use_release_issue()") tr <- target_repo(github_get = TRUE) if (!isTRUE(tr$can_push)) { - ui_line(" - It is very unusual to open a release issue on a repo you can't push to: - {ui_value(tr$repo_spec)}") - if (ui_nope("Do you really want to do this?")) { - ui_oops("Cancelling.") + ui_bullets(c( + "!" = "It is very unusual to open a release issue on a repo you can't push + to ({.val {tr$repo_spec}})." + )) + if (ui_nah("Do you really want to do this?")) { + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } @@ -262,7 +263,7 @@ use_github_release <- function(publish = TRUE, gh <- gh_tr(tr) - ui_cli_inform("Publishing {tag_name} release to GitHub") + ui_bullets("Publishing {tag_name} release to GitHub") release <- gh( "POST /repos/{owner}/{repo}/releases", name = release_name, @@ -271,10 +272,10 @@ use_github_release <- function(publish = TRUE, body = news, draft = !publish ) - ui_cli_inform("Release at {.url {release$html_url}}") + ui_bullets("Release at {.url {release$html_url}}") if (!is.null(dat$file)) { - ui_cli_inform("Deleting {.path {dat$file}}") + ui_bullets("Deleting {.path {dat$file}}") file_delete(dat$file) } @@ -286,7 +287,7 @@ get_release_data <- function(tr = target_repo(github_get = TRUE)) { path_first_existing(proj_path(c("CRAN-SUBMISSION", "CRAN-RELEASE"))) if (is.null(cran_submission)) { - ui_done("Using current HEAD commit for the release") + ui_bullets(c("v" = "Using current HEAD commit for the release.")) challenge_non_default_branch() check_branch_pushed() return(list( @@ -345,8 +346,9 @@ get_release_data <- function(tr = target_repo(github_get = TRUE)) { out$Package <- project_name() out$file <- cran_submission - ui_done(" - {ui_path(out$file)} file found, from a submission on {as.Date(out$Date)}") + ui_bullets(c( + "{.path {pth(out$file)}} file found, from a submission on {as.Date(out$Date)}." + )) out } @@ -362,11 +364,12 @@ check_github_has_SHA <- function(SHA = gert::git_info(repo = git_repo())$commit, return() } if (inherits(SHA_GET$error, "http_error_404")) { - ui_stop(" - Can't find SHA {ui_value(substr(SHA, 1, 7))} in {ui_value(tr$repo_spec)}. - Do you need to push?") + ui_abort(c( + "Can't find SHA {.val {substr(SHA, 1, 7)}} in {.val {tr$repo_spec}}.", + "Do you need to push?" + )) } - ui_stop("Internal error: Unexpected error when checking for SHA on GitHub") + ui_abort("Internal error: Unexpected error when checking for SHA on GitHub.") } get_release_news <- function(SHA = gert::git_info(repo = git_repo())$commit, @@ -390,10 +393,11 @@ get_release_news <- function(SHA = gert::git_info(repo = git_repo())$commit, } if (is.null(news)) { - ui_oops(" - Can't find {ui_path('NEWS.md')} in the released package source. - usethis consults this file for release notes. - Call {ui_code('usethis::use_news_md()')} to set this up for the future.") + ui_bullets(c( + "x" = "Can't find {.path {pth('NEWS.md')}} in the released package source.", + "i" = "{.pkg usethis} consults this file for release notes.", + "i" = "Call {.run usethis::use_news_md()} to set this up for the future." + )) if (on_cran) "-- no release notes --" else "Initial release" } else { news_latest(news) @@ -433,7 +437,7 @@ news_latest <- function(lines) { headings <- which(grepl("^#\\s+", lines)) if (length(headings) == 0) { - ui_stop("No top-level headings found in {ui_value('NEWS.md')}") + ui_abort("No top-level headings found in {.path {pth('NEWS.md')}}.") } else if (length(headings) == 1) { news <- lines[seq2(headings + 1, length(lines))] } else { diff --git a/R/rename-files.R b/R/rename-files.R index e856b6d41..5f71605b8 100644 --- a/R/rename-files.R +++ b/R/rename-files.R @@ -24,7 +24,9 @@ rename_files <- function(old, new) { r_old_path <- proj_path("R", old, ext = "R") r_new_path <- proj_path("R", new, ext = "R") if (file_exists(r_old_path)) { - ui_done("Moving {ui_path(r_old_path)} to {ui_path(r_new_path)}") + ui_bullets(c( + "v" = "Moving {.path {pth(r_old_path)}} to {.path {pth(r_new_path)}}." + )) file_move(r_old_path, r_new_path) } @@ -36,7 +38,9 @@ rename_files <- function(old, new) { src_new <- path(path_dir(src_old), src_new_file) if (length(src_old) > 1) { - ui_done("Moving {ui_path(src_old)} to {ui_path(src_new)}") + ui_bullets(c( + "v" = "Moving {.path {pth(src_old)}} to {.path {pth(src_new)}}." + )) file_move(src_old, src_new) } } @@ -57,7 +61,9 @@ rename_files <- function(old, new) { ) new_test <- rename_test(old_test) if (length(old_test) > 0) { - ui_done("Moving {ui_path(old_test)} to {ui_path(new_test)}") + ui_bullets(c( + "v" = "Moving {.path {pth(old_test)}} to {.path {pth(new_test)}}." + )) file_move(old_test, new_test) } snaps_dir <- proj_path("tests", "testthat", "_snaps") @@ -65,7 +71,9 @@ rename_files <- function(old, new) { old_snaps <- dir_ls(snaps_dir, glob = glue("*/{old}.md")) if (length(old_snaps) > 0) { new_snaps <- rename_test(old_snaps) - ui_done("Moving {ui_path(old_snaps)} to {ui_path(new_snaps)}") + ui_bullets(c( + "v" = "Moving {.path {pth(old_snaps)}} to {.path {pth(new_snaps)}}." + )) file_move(old_snaps, new_snaps) } } @@ -81,7 +89,7 @@ rename_files <- function(old, new) { # Remove old context lines context <- grepl("context\\(.*\\)", lines) if (any(context)) { - ui_done("Removing call to {ui_code('context()')}") + ui_bullets(c("v" = "Removing call to {.fun context}.")) lines <- lines[!context] if (lines[[1]] == "") { lines <- lines[-1] @@ -92,7 +100,7 @@ rename_files <- function(old, new) { new_test <- new_test[new_test != test_path] if (length(old_test) > 0) { - ui_done("Updating paths in {ui_path(test_path)}") + ui_bullets(c("v" = "Updating paths in {.path {pth(test_path)}}.")) for (i in seq_along(old_test)) { lines <- gsub(path_file(old_test[[i]]), path_file(new_test[[i]]), lines, fixed = TRUE) diff --git a/R/revdep.R b/R/revdep.R index 2ed41f49c..4621e2d83 100644 --- a/R/revdep.R +++ b/R/revdep.R @@ -20,6 +20,8 @@ use_revdep <- function() { ) ) - ui_todo("Run checks with {ui_code('revdepcheck::revdep_check(num_workers = 4)')}") + ui_bullets(c( + "_" = "Run checks with {.run revdepcheck::revdep_check(num_workers = 4)}." + )) invisible() } diff --git a/R/roxygen.R b/R/roxygen.R index e02eb30b2..de1727b12 100644 --- a/R/roxygen.R +++ b/R/roxygen.R @@ -18,7 +18,7 @@ use_roxygen_md <- function(overwrite = FALSE) { proj_desc_field_update("Roxygen", "list(markdown = TRUE)", overwrite = FALSE) proj_desc_field_update("RoxygenNote", roxy_ver, overwrite = FALSE) - ui_todo("Run {ui_code('devtools::document()')}") + ui_bullets(c("_" = "Run {.run devtools::document()}.")) return(invisible()) } @@ -32,22 +32,25 @@ use_roxygen_md <- function(overwrite = FALSE) { proj_desc_field_update("Roxygen", "list(markdown = TRUE)", overwrite = TRUE) check_installed("roxygen2md") - ui_todo(" - Run {ui_code('roxygen2md::roxygen2md()')} to convert existing Rd \\ - comments to markdown") + ui_bullets(c( + "_" = "Run {.run roxygen2md::roxygen2md()} to convert existing Rd + comments to markdown." + )) if (!uses_git()) { - ui_todo(" - Consider using Git for greater visibility into and control over the \\ - conversion process") + ui_bullets(c( + "!" = "Consider using Git for greater visibility into and control over + the conversion process." + )) } - ui_todo("Run {ui_code('devtools::document()')} when you're done") + ui_bullets(c("v" = "Run {.run devtools::document()} when you're done.")) return(invisible()) } - ui_stop(" - {ui_path('DESCRIPTION')} already has a {ui_field('Roxygen')} field - Delete it and try again or call {ui_code('use_roxygen_md(overwrite = TRUE)')}") + ui_abort(c( + "DESCRIPTION already has a {.field Roxygen} field.", + "Delete that field and try again or call {.code use_roxygen_md(overwrite = TRUE)}." + )) invisible() } @@ -77,7 +80,7 @@ uses_roxygen <- function() { roxygen_ns_append <- function(tag) { block_append( - glue("{ui_value(tag)}"), + tag, glue("#' {tag}"), path = proj_path(package_doc_path()), block_start = "## usethis namespace: start", @@ -96,18 +99,20 @@ roxygen_ns_show <- function() { } roxygen_remind <- function() { - ui_todo("Run {ui_code('devtools::document()')} to update {ui_path('NAMESPACE')}") + ui_bullets(c( + "_" = "Run {.run devtools::document()} to update {.path {pth('NAMESPACE')}}." + )) TRUE } roxygen_update_ns <- function(load = is_interactive()) { - ui_done("Writing {ui_path('NAMESPACE')}") + ui_bullets(c("v" = "Writing {.path {pth('NAMESPACE')}}.")) utils::capture.output( suppressMessages(roxygen2::roxygenise(proj_get(), "namespace")) ) if (load) { - ui_done("Loading {project_name()}") + ui_bullets(c("v" = "Loading {.pkg {project_name()}}.")) pkgload::load_all(path = proj_get(), quiet = TRUE) } @@ -123,11 +128,9 @@ check_uses_roxygen <- function(whos_asking) { return(invisible()) } - ui_stop( - " - Project {ui_value(project_name())} does not use roxygen2. - {ui_code(whos_asking)} can not work without it. - You might just need to run {ui_code('devtools::document()')} once, then try again. - " - ) + ui_abort(c( + "Package {.pkg {project_name()}} does not use roxygen2.", + "{.fun {whos_asking}} can not work without it.", + "You might just need to run {.run devtools::document()} once, then try again." + )) } diff --git a/R/rprofile.R b/R/rprofile.R index cd3f549a4..68f176a5e 100644 --- a/R/rprofile.R +++ b/R/rprofile.R @@ -39,34 +39,28 @@ use_devtools <- function() { use_rprofile_package <- function(package) { check_installed(package) - ui_todo( - "Include this code in {ui_value('.Rprofile')} to make \\ - {ui_field(package)} available in all interactive sessions." - ) - ui_code_block( - " + ui_bullets(c( + "_" = "Include this code in {.path .Rprofile} to make {.pkg {package}} + available in all interactive sessions:" + )) + ui_code_snippet(" if (interactive()) {{ suppressMessages(require({package})) - }} - " - ) + }}") edit_r_profile("user") } #' @rdname rprofile-helper #' @export use_partial_warnings <- function() { - ui_todo( - "Include this code in {ui_path('.Rprofile')} to warn on partial matches." - ) - ui_code_block( - " + ui_bullets(c( + "_" = "Include this code in {.path .Rprofile} to warn on partial matches:" + )) + ui_code_snippet(" options( warnPartialMatchArgs = TRUE, warnPartialMatchDollar = TRUE, warnPartialMatchAttr = TRUE - ) - " - ) + )") edit_r_profile("user") } diff --git a/R/rstudio.R b/R/rstudio.R index 427ee5055..10f43c910 100644 --- a/R/rstudio.R +++ b/R/rstudio.R @@ -167,14 +167,14 @@ restart_rstudio <- function(message = NULL) { } if (!is.null(message)) { - ui_todo(message) + ui_bullets(message) } if (!rstudioapi::hasFun("openProject")) { return(FALSE) } - if (ui_nope("Restart now?")) { + if (ui_nah("Restart now?")) { return(FALSE) } @@ -225,7 +225,7 @@ use_rstudio_preferences <- function(...) { next } - ui_done("Setting RStudio preference {ui_field(name)} to {ui_value(val)}.") + ui_bullets(c("v" = "Setting RStudio preference {.field {name}} to {.val {val}}.")) json[[name]] <- val } diff --git a/R/sitrep.R b/R/sitrep.R index 316c2bb8d..6637fddc7 100644 --- a/R/sitrep.R +++ b/R/sitrep.R @@ -49,60 +49,52 @@ print.sitrep <- function(x, ...) { x[["active_rstudio_proj"]] != x[["active_usethis_proj"]] if (rstudio_available() && !rstudio_proj_is_active) { - ui_todo( - " - You are working in RStudio, but are not in an RStudio Project. - A Project-based workflow offers many advantages. Read more at: - {ui_field('https://support.rstudio.com/hc/en-us/articles/200526207-Using-Projects')} - {ui_field('https://whattheyforgot.org/project-oriented-workflow.html')} - " - ) + ui_bullets(c( + "i" = "You are working in RStudio, but are not in an RStudio Project.", + "i" = "A Project-based workflow offers many advantages. Read more at:", + " " = "{.url https://docs.posit.co/ide/user/ide/guide/code/projects.html}", + " " = "{.url https://rstats.wtf/projects}" + )) } if (!usethis_proj_is_active) { - ui_todo( - " - There is currently no active usethis project. - usethis attempts to activate a project upon first need. - Call {ui_code('proj_get()')} to initiate project discovery. - Call {ui_code('proj_set(\"path/to/project\")')} or \\ - {ui_code('proj_activate(\"path/to/project\")')} to provide - an explicit path. - " - ) + ui_bullets(c( + "i" = "There is currently no active {.pkg usethis} project.", + "i" = "{.pkg usethis} attempts to activate a project upon first need.", + "_" = "Call {.run usethis::proj_get()} to initiate project discovery.", + "_" = 'Call {.code proj_set("path/to/project")} or + {.code proj_activate("path/to/project")} to provide an explicit + path.' + )) } if (usethis_proj_is_not_wd) { - ui_todo( - " - Your working directory is not the same as the active usethis project. - Set working directory to the project: {ui_code('setwd(proj_get())')} - Set project to working directory: {ui_code('proj_set(getwd())')} - " - ) + ui_bullets(c( + "i" = "Your working directory is not the same as the active usethis project.", + "_" = "Set working directory to the project: {.code setwd(proj_get())}.", + "_" = "Set project to working directory: {.code usethis::proj_set(getwd())}." + )) } if (rstudio_proj_is_not_wd) { - ui_todo( - " - Your working directory is not the same as the active RStudio Project. - Set working directory to the Project: {ui_code('setwd(rstudioapi::getActiveProject())')} - " - ) + ui_bullets(c( + "i" = "Your working directory is not the same as the active RStudio Project.", + "_" = "Set working directory to the Project: + {.code setwd(rstudioapi::getActiveProject())}." + )) } if (usethis_proj_is_not_rstudio_proj) { - ui_todo( - " - Your active RStudio Project is not the same as the active usethis project. - Set usethis project to RStudio Project: \\ - {ui_code('proj_set(rstudioapi::getActiveProject())')} - Restart RStudio in the usethis project: \\ - {ui_code('rstudioapi::openProject(proj_get())')} - Open the usethis project in a new instance of RStudio: \\ - {ui_code('proj_activate(proj_get())')} - " - ) + ui_bullets(c( + "i" = "Your active RStudio Project is not the same as the active + {.pkg usethis} project.", + "_" = "Set active {.pkg usethis} project to RStudio Project: + {.code usethis::proj_set(rstudioapi::getActiveProject())}.", + "_" = "Restart RStudio in the active {.pkg usethis} project: + {.code rstudioapi::openProject(usethis::proj_get())}.", + "_" = "Open the active {.pkg usethis} project in a new instance of RStudio: + {.code usethis::proj_activate(usethis::proj_get())}." + )) } invisible(x) diff --git a/R/spelling.R b/R/spelling.R index f53ad20f0..b62942c02 100644 --- a/R/spelling.R +++ b/R/spelling.R @@ -24,5 +24,5 @@ use_spell_check <- function(vignettes = TRUE, spelling::spell_check_setup( pkg = proj_get(), vignettes = vignettes, lang = lang, error = error ) - ui_todo("Run {ui_code('devtools::check()')} to trigger spell check") + ui_bullets(c("_" = "Run {.run devtools::check()} to trigger spell check.")) } diff --git a/R/template.R b/R/template.R index d001b7c17..c8504f93a 100644 --- a/R/template.R +++ b/R/template.R @@ -69,10 +69,9 @@ find_template <- function(template_name, package = "usethis") { error = function(e) "" ) if (identical(path, "")) { - ui_stop( - "Could not find template {ui_value(template_name)} \\ - in package {ui_value(package)}." - ) + ui_abort(" + Could not find template {.val {template_name}} in package {.pkg package} + package.") } path } diff --git a/R/test.R b/R/test.R index f317d8b08..057217b2b 100644 --- a/R/test.R +++ b/R/test.R @@ -21,16 +21,17 @@ use_testthat <- function(edition = NULL, parallel = FALSE) { use_testthat_impl(edition, parallel = parallel) - ui_todo( - "Call {ui_code('use_test()')} to initialize a basic test file and open it \\ - for editing." - ) + ui_bullets(c( + "_" = "Call {.run usethis::use_test()} to initialize a basic test file and + open it for editing." + )) } use_testthat_impl <- function(edition = NULL, parallel = FALSE) { check_installed("testthat") if (utils::packageVersion("testthat") < "2.1.0") { - ui_stop("testthat 2.1.0 or greater needed. Please install before re-trying") + ui_abort(" + {.pkg testthat} 2.1.0 or greater needed. Please install before re-trying") } if (is_package()) { @@ -46,7 +47,7 @@ use_testthat_impl <- function(edition = NULL, parallel = FALSE) { } } else { if (!is.null(edition)) { - ui_stop("Can't declare testthat edition outside of a package") + ui_abort("Can't declare {.pkg testthat} edition outside of a package.") } } @@ -70,11 +71,13 @@ check_edition <- function(edition = NULL) { version } else { if (!is.numeric(edition) || length(edition) != 1) { - ui_stop("`edition` must be a single number") + ui_abort("{.arg edition} must be a single number.") } if (edition > version) { vers <- utils::packageVersion("testthat") - ui_stop("`edition` ({edition}) not available in installed testthat ({vers})") + ui_abort(" + {.var edition} ({edition}) not available in installed verion of + {.pkg testthat} ({vers}).") } as.integer(edition) } diff --git a/R/tibble.R b/R/tibble.R index 2e127a203..abb00b5fb 100644 --- a/R/tibble.R +++ b/R/tibble.R @@ -33,8 +33,12 @@ use_tibble <- function() { created <- use_import_from("tibble", "tibble") - ui_todo("Document a returned tibble like so:") - ui_code_block("#' @return a [tibble][tibble::tibble-package]", copy = FALSE) + ui_bullets(c("_" = "Document a returned tibble like so:")) + ui_code_snippet( + "#' @return a [tibble][tibble::tibble-package]", + language = "", + copy = FALSE + ) invisible(created) } diff --git a/R/tidyverse.R b/R/tidyverse.R index faa9f58eb..4daa46b88 100644 --- a/R/tidyverse.R +++ b/R/tidyverse.R @@ -85,13 +85,15 @@ create_tidy_package <- function(path, copyright_holder = NULL) { use_cran_comments(open = FALSE) - ui_todo("In the new package, remember to do:") - ui_todo("{ui_code('use_git()')}") - ui_todo("{ui_code('use_github()')}") - ui_todo("{ui_code('use_tidy_github()')}") - ui_todo("{ui_code('use_tidy_github_actions()')}") - ui_todo("{ui_code('use_tidy_github_labels()')}") - ui_todo("{ui_code('use_pkgdown_github_pages()')}") + ui_bullets(c( + "i" = "In the new package, remember to do:", + "_" = "{.run usethis::use_git()}", + "_" = "{.run usethis::use_github()}", + "_" = "{.run usethis::use_tidy_github()}", + "_" = "{.run usethis::use_tidy_github_actions()}", + "_" = "{.run usethis::use_tidy_github_labels()}", + "_" = "{.run usethis::use_pkgdown_github_pages()}" + )) proj_activate(path) } @@ -231,8 +233,10 @@ use_tidy_style <- function(strict = TRUE) { strict = strict ) } - ui_line() - ui_done("Styled project according to the tidyverse style guide") + ui_bullets(c( + " " = "", + "v" = "Styled project according to the tidyverse style guide." + )) invisible(styled) } @@ -297,9 +301,10 @@ use_tidy_thanks <- function(repo_spec = NULL, from_timestamp <- as_timestamp(repo_spec, x = from) %||% "2008-01-01" to_timestamp <- as_timestamp(repo_spec, x = to) - ui_done(" - Looking for contributors from {as.Date(from_timestamp)} to \\ - {to_timestamp %||% 'now'}") + ui_bullets(c( + "i" = "Looking for contributors from {as.Date(from_timestamp)} to + {to_timestamp %||% 'now'}." + )) res <- gh::gh( "/repos/{owner}/{repo}/issues", @@ -310,7 +315,7 @@ use_tidy_thanks <- function(repo_spec = NULL, .limit = Inf ) if (length(res) < 1) { - ui_oops("No matching issues/PRs found") + ui_bullets(c("x" = "No matching issues/PRs found.")) return(invisible()) } @@ -324,15 +329,18 @@ use_tidy_thanks <- function(repo_spec = NULL, res <- res[creation_time(res) <= as.POSIXct(to_timestamp)] } if (length(res) == 0) { - ui_line("No matching issues/PRs found.") + ui_bullets(c("x" = "No matching issues/PRs found.")) return(invisible()) } contributors <- sort(unique(map_chr(res, c("user", "login")))) contrib_link <- glue("[@{contributors}](https://github.com/{contributors})") - ui_done("Found {length(contributors)} contributors:") - ui_code_block(glue_collapse(contrib_link, sep = ", ", last = ", and ") + glue(".")) + ui_bullets(c("v" = "Found {length(contributors)} contributors:")) + ui_code_snippet( + glue_collapse(contrib_link, sep = ", ", last = ", and ") + glue("."), + language = "" + ) invisible(contributors) } @@ -347,7 +355,7 @@ as_timestamp <- function(repo_spec, x = NULL) { if (inherits(as_POSIXct, "POSIXct")) { return(x) } - ui_done("Resolving timestamp for ref {ui_value(x)}") + ui_bullets(c("v" = "Resolving timestamp for ref {.val {x}}.")) ref_df(repo_spec, refs = x)$timestamp } @@ -411,7 +419,7 @@ base_and_recommended <- function() { #' @export use_tidy_logo <- function(geometry = "240x278", retina = TRUE) { if (!is_posit_pkg()) { - ui_stop("This function can only be used for Posit packages") + ui_abort("This function only works for Posit packages.") } tf <- withr::local_tempfile(fileext = ".png") diff --git a/R/ui.R b/R/ui-legacy.R similarity index 75% rename from R/ui.R rename to R/ui-legacy.R index e1a11405e..92473119e 100644 --- a/R/ui.R +++ b/R/ui-legacy.R @@ -1,9 +1,22 @@ -#' User interface +#' Legacy functions related to user interface #' #' @description -#' These functions are used to construct the user interface of usethis. Use -#' them in your own package so that your `use_` functions work the same way -#' as usethis. +#' `r lifecycle::badge("superseded")` +#' + +#' These functions are now superseded. External users of the `usethis::ui_*()` +#' functions are encouraged to use the [cli package](https://cli.r-lib.org/) +#' instead. The cli package did not have the required functionality when the +#' `usethis::ui_*()` functions were created, but it has had that for a while +#' now and it's the superior option. There is even a cli vignette about how to +#' make this transition: `vignette("usethis-ui", package = "cli")`. +#' +#' usethis itself now uses cli internally for its UI, but these new functions +#' are not exported and presumably never will be. There is a developer-focused +#' article on the process of transitioning usethis's own UI to use cli (LINK +#' TO COME). + +#' @details #' #' The `ui_` functions can be broken down into four main categories: #' @@ -17,9 +30,8 @@ #' The question functions [ui_yeah()] and [ui_nope()] have their own [help #' page][ui-questions]. #' -#' @section Silencing output: #' All UI output (apart from `ui_yeah()`/`ui_nope()` prompts) can be silenced -#' by setting `options(usethis.quiet = TRUE)`. Use `ui_silence()` to silence +#' by setting `options(usethis.quiet = TRUE)`. Use [ui_silence()] to silence #' selected actions. #' #' @param x A character vector. @@ -34,8 +46,7 @@ #' @return The block styles, conditions, and questions are called for their #' side-effect. The inline styles return a string. #' @keywords internal -#' @family user interface functions -#' @name ui +#' @name ui-legacy-functions #' @examples #' new_val <- "oxnard" #' ui_done("{ui_field('name')} set to {ui_value(new_val)}") @@ -50,7 +61,7 @@ NULL # Block styles ------------------------------------------------------------ -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_line <- function(x = character(), .envir = parent.frame()) { x <- glue_collapse(x, "\n") @@ -58,41 +69,41 @@ ui_line <- function(x = character(), .envir = parent.frame()) { ui_inform(x) } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_todo <- function(x, .envir = parent.frame()) { x <- glue_collapse(x, "\n") x <- glue(x, .envir = .envir) - ui_bullet(x, crayon::red(cli::symbol$bullet)) + ui_legacy_bullet(x, crayon::red(cli::symbol$bullet)) } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_done <- function(x, .envir = parent.frame()) { x <- glue_collapse(x, "\n") x <- glue(x, .envir = .envir) - ui_bullet(x, crayon::green(cli::symbol$tick)) + ui_legacy_bullet(x, crayon::green(cli::symbol$tick)) } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_oops <- function(x, .envir = parent.frame()) { x <- glue_collapse(x, "\n") x <- glue(x, .envir = .envir) - ui_bullet(x, crayon::red(cli::symbol$cross)) + ui_legacy_bullet(x, crayon::red(cli::symbol$cross)) } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_info <- function(x, .envir = parent.frame()) { x <- glue_collapse(x, "\n") x <- glue(x, .envir = .envir) - ui_bullet(x, crayon::yellow(cli::symbol$info)) + ui_legacy_bullet(x, crayon::yellow(cli::symbol$info)) } #' @param copy If `TRUE`, the session is interactive, and the clipr package #' is installed, will copy the code block to the clipboard. -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_code_block <- function(x, copy = rlang::is_interactive(), @@ -113,7 +124,7 @@ ui_code_block <- function(x, # Conditions -------------------------------------------------------------- -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_stop <- function(x, .envir = parent.frame()) { x <- glue_collapse(x, "\n") @@ -127,7 +138,7 @@ ui_stop <- function(x, .envir = parent.frame()) { stop(cnd) } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_warn <- function(x, .envir = parent.frame()) { x <- glue_collapse(x, "\n") @@ -137,24 +148,20 @@ ui_warn <- function(x, .envir = parent.frame()) { } -# Silence ----------------------------------------------------------------- - -#' @rdname ui -#' @param code Code to execute with usual UI output silenced. -#' @export -ui_silence <- function(code) { - withr::with_options(list(usethis.quiet = TRUE), code) -} # Questions --------------------------------------------------------------- - #' User interface - Questions #' -#' These functions are used to interact with the user by posing a simple yes or -#' no question. For details on the other `ui_*()` functions, see the [ui] help -#' page. +#' @description +#' `r lifecycle::badge("superseded")` +#' + +#' `ui_yeah()` and `ui_nope()` are technically superseded, but, unlike the rest +#' of the legacy [`ui_*()`][ui-legacy-functions] functions, there's not yet a +#' drop-in replacement available in the [cli package](https://cli.r-lib.org/). +#' `ui_yeah()` and `ui_nope()` are no longer used internally in usethis. #' -#' @inheritParams ui +#' @inheritParams ui-legacy-functions #' @param yes A character vector of "yes" strings, which are randomly sampled to #' populate the menu. #' @param no A character vector of "no" strings, which are randomly sampled to @@ -170,7 +177,6 @@ ui_silence <- function(code) { #' of `ui_yeah()`. #' @name ui-questions #' @keywords internal -#' @family user interface functions #' @examples #' \dontrun{ #' ui_yeah("Do you like R?") @@ -206,6 +212,9 @@ ui_yeah <- function(x, } # TODO: should this be ui_inform()? + # later observation: probably not? you would not want these prompts to be + # suppressed when `usethis.quiet = TRUE`, i.e. if the menu() appears, then + # the introduction should also always appear rlang::inform(x) out <- utils::menu(qs) out != 0L && qs[[out]] %in% yes @@ -229,7 +238,7 @@ ui_nope <- function(x, # Inline styles ----------------------------------------------------------- -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_field <- function(x) { x <- crayon::green(x) @@ -237,7 +246,7 @@ ui_field <- function(x) { x } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_value <- function(x) { if (is.character(x)) { @@ -248,25 +257,14 @@ ui_value <- function(x) { x } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export #' @param base If specified, paths will be displayed relative to this path. ui_path <- function(x, base = NULL) { - is_directory <- is_dir(x) | grepl("/$", x) - if (is.null(base)) { - x <- proj_rel_path(x) - } else if (!identical(base, NA)) { - x <- path_rel(x, base) - } - - # rationalize trailing slashes - x <- path_tidy(x) - x <- ifelse(is_directory, paste0(x, "/"), x) - - ui_value(x) + ui_value(ui_path_impl(x, base = base)) } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_code <- function(x) { x <- encodeString(x, quote = "`") @@ -275,7 +273,7 @@ ui_code <- function(x) { x } -#' @rdname ui +#' @rdname ui-legacy-functions #' @export ui_unset <- function(x = "unset") { check_string(x) @@ -291,7 +289,7 @@ indent <- function(x, first = " ", indent = first) { paste0(first, x) } -ui_bullet <- function(x, bullet = cli::symbol$bullet) { +ui_legacy_bullet <- function(x, bullet = cli::symbol$bullet) { bullet <- paste0(bullet, " ") x <- indent(x, bullet, " ") ui_inform(x) @@ -305,29 +303,3 @@ ui_inform <- function(...) { } invisible() } - -is_quiet <- function() { - isTRUE(getOption("usethis.quiet", default = FALSE)) -} - -# Sitrep helpers --------------------------------------------------------------- - -hd_line <- function(name) { - ui_inform(crayon::bold(name)) -} - -kv_line <- function(key, value, .envir = parent.frame()) { - value <- if (is.null(value)) ui_unset() else ui_value(value) - key <- glue(key, .envir = .envir) - ui_inform(glue("{cli::symbol$bullet} {key}: {value}")) -} - - -# cli wrappers ------------------------------------------------------------ - -ui_cli_inform <- function(..., .envir = parent.frame()) { - if (!is_quiet()) { - cli::cli_inform(..., .envir = .envir) - } - invisible() -} diff --git a/R/upkeep.R b/R/upkeep.R index 51db4fd14..a24ba0993 100644 --- a/R/upkeep.R +++ b/R/upkeep.R @@ -29,11 +29,12 @@ make_upkeep_issue <- function(year, tidy) { tr <- target_repo(github_get = TRUE) if (!isTRUE(tr$can_push)) { - ui_line(" - It is very unusual to open an upkeep issue on a repo you can't push to: - {ui_value(tr$repo_spec)}") - if (ui_nope("Do you really want to do this?")) { - ui_oops("Cancelling.") + ui_bullets(c( + "!" = "It is very unusual to open an upkeep issue on a repo you can't push + to ({.val {tr$repo_spec}})." + )) + if (ui_nah("Do you really want to do this?")) { + ui_bullets(c("x" = "Cancelling.")) return(invisible()) } } diff --git a/R/use_github_file.R b/R/use_github_file.R index f5f131ced..afba90e27 100644 --- a/R/use_github_file.R +++ b/R/use_github_file.R @@ -72,7 +72,9 @@ use_github_file <- function(repo_spec, ref_string <- if (is.null(ref)) "" else glue("@{ref}") github_string <- glue("{repo_spec}/{path}{ref_string}") - ui_done("Saving {ui_path(github_string)} to {ui_path(save_as)}") + ui_bullets(c( + "v" = "Saving {.val {github_string}} to {.path {pth(save_as)}}." + )) lines <- read_github_file( repo_spec = repo_spec, @@ -137,13 +139,13 @@ parse_file_url <- function(x) { # TODO: generalize here for GHE hosts that don't include 'github' if (!grepl("github", dat$host)) { - ui_stop("URL doesn't seem to be associated with GitHub.") + ui_abort("URL doesn't seem to be associated with GitHub.") } if (!grepl("^(raw[.])?github", dat$host) || !nzchar(dat$fragment) || (grepl("^github", dat$host) && !grepl("^/blob/", dat$fragment))) { - ui_stop("Can't parse the URL provided via {ui_code('repo_spec')}.") + ui_abort("Can't parse the URL provided via {.arg repo_spec}.") } out$parsed <- TRUE diff --git a/R/use_import_from.R b/R/use_import_from.R index d31a702e8..c27607294 100644 --- a/R/use_import_from.R +++ b/R/use_import_from.R @@ -19,11 +19,11 @@ #' @export #' @examples #' \dontrun{ -#' use_import_from("usethis", "ui_todo") +#' use_import_from("glue", "glue") #' } use_import_from <- function(package, fun, load = is_interactive()) { if (!is_string(package)) { - ui_stop("{ui_code('package')} must be a single string") + ui_abort("{.arg package} must be a single string.") } check_is_package("use_import_from()") check_uses_roxygen("use_import_from()") @@ -49,8 +49,8 @@ check_fun_exists <- function(package, fun) { if (exists(fun, envir = asNamespace(package))) { return() } - name <- paste0(package, "::", fun, "()") - ui_stop("Can't find {ui_code(name)}") + name <- paste0(package, "::", fun) + ui_abort("Can't find {.fun {name}}.") } check_has_package_doc <- function(whos_asking) { @@ -59,15 +59,15 @@ check_has_package_doc <- function(whos_asking) { } msg <- c( - "{ui_code(whos_asking)} requires package-level documentation.", - "Would you like to add it now?" + "!" = "{.fun {whos_asking}} requires package-level documentation.", + " " = "Would you like to add it now?" ) - if (is_interactive() && ui_yeah(msg)) { + if (is_interactive() && ui_yep(msg)) { use_package_doc() } else { - ui_stop(c( - "{ui_code(whos_asking)} requires package docs", - "You can add it by running {ui_code('use_package_doc()')}" + ui_abort(c( + "{.fun {whos_asking}} requires package-level documentation.", + "You can add it by running {.run usethis::use_package_doc()}." )) } diff --git a/R/usethis-defunct.R b/R/usethis-defunct.R index 7b1cbf327..6d5877df4 100644 --- a/R/usethis-defunct.R +++ b/R/usethis-defunct.R @@ -19,15 +19,10 @@ pr_pull_upstream <- function() { #' @rdname pr_pull_upstream #' @export pr_sync <- function() { - details <- glue(" - Sync a PR with: - * {ui_code('pr_pull()')} - * {ui_code('pr_merge_main()')} - * {ui_code('pr_push()')}") lifecycle::deprecate_stop( when = "2.0.0", what = "pr_sync()", - details = details + details = "Sync a PR with:`pr_pull(); pr_merge_main(); pr_push();`" ) } @@ -65,9 +60,9 @@ browse_github_pat <- function(...) { #' @export github_token <- function() { details <- glue(" - Call {ui_code('gh::gh_token()')} to retrieve a GitHub personal access token - Call {ui_code('gh_token_help()')} if you need help getting or configuring \\ - your token") + Call `gh::gh_token()` to retrieve a GitHub personal access token. + Call `usethis::gh_token_help()` if you need help getting or configuring \\ + your token.") lifecycle::deprecate_stop( "2.0.0", what = "github_token()", @@ -190,9 +185,9 @@ deprecate_warn_host <- function(whos_asking, details = NULL) { what <- glue("{whos_asking}(host = )") host_explanation <- glue(" - usethis now determines the {ui_code('host')} from the current project's \\ + usethis now determines the `host` from the current project's \\ Git remotes. - The {ui_code('host')} argument is ignored and will eventually be removed.") + The `host` argument is ignored and will eventually be removed.") lifecycle::deprecate_warn( "2.0.0", @@ -209,8 +204,7 @@ deprecate_warn_auth_token <- function(whos_asking, details = NULL) { usethis now delegates token lookup to the gh package, which retrieves \\ credentials based on the targeted host URL. This URL is determined by the current project's Git remotes. - The {ui_code('auth_token')} argument is ignored and will eventually be \\ - removed.") + The `auth_token` argument is ignored and will eventually be removed.") lifecycle::deprecate_warn( "2.0.0", @@ -226,8 +220,7 @@ deprecate_warn_repo_spec <- function(whos_asking, details = NULL) { repo_spec_explanation <- glue(" usethis now consults the current project's Git remotes to determine the \\ target repo. - The {ui_code('repo_spec')} argument is ignored and will eventually be \\ - removed.") + The `repo_spec` argument is ignored and will eventually be removed.") lifecycle::deprecate_warn( "2.0.0", @@ -412,9 +405,8 @@ use_github_action_check_full <- function(save_as = "R-CMD-check.yaml", details <- glue(" It is overkill for the vast majority of R packages. The \"check-full\" workflow is among those configured by \\ - {ui_code('use_tidy_github_actions()')}. - If you really want it, request it by name with \\ - {ui_code('use_github_action()')}.") + `use_tidy_github_actions()`. + If you really want it, request it by name with `use_github_action()`.") lifecycle::deprecate_stop( "2.1.0", "use_github_action_check_full()", diff --git a/R/utils-git.R b/R/utils-git.R index 234c64444..c1716ffdc 100644 --- a/R/utils-git.R +++ b/R/utils-git.R @@ -32,9 +32,9 @@ check_uses_git <- function() { return(invisible()) } - ui_stop(c( + ui_abort(c( "Cannot detect that project is already a Git repository.", - "Do you need to run {ui_code('use_git()')}?" + "Do you need to run {.run usethis::use_git()}?" )) } @@ -111,7 +111,9 @@ ensure_core_excludesFile <- function() { # express path relative to user's home directory, except on Windows path <- path("~", path_rel(path, path_home())) } - ui_done("Configuring {ui_field('core.excludesFile')}: {ui_path(path)}") + ui_bullets(c( + "v" = "Configuring {.field core.excludesFile}: {.path {pth(path)}}" + )) gert::git_config_global_set("core.excludesFile", path) invisible() } @@ -135,9 +137,9 @@ git_ask_commit <- function(message, untracked, push = FALSE, paths = NULL) { # this is defined here to encourage all commits to route through this function git_commit <- function(paths, message) { repo <- git_repo() - ui_done("Adding files") + ui_bullets(c("v" = "Adding files.")) gert::git_add(paths, repo = repo) - ui_done("Making a commit with message {ui_value(message)}") + ui_bullets(c("v" = "Making a commit with message {.val {message}}.")) gert::git_commit(message, repo = repo) } @@ -153,31 +155,19 @@ git_ask_commit <- function(message, untracked, push = FALSE, paths = NULL) { } paths <- sort(paths) - ui_paths <- map_chr(paths, ui_path) - if (n > 10) { - ui_paths <- c(ui_paths[1:10], "...") - } - - if (n == 1) { - file_hint <- "There is 1 uncommitted file:" - } else { - file_hint <- "There are {n} uncommitted files:" - } - ui_line(c( - file_hint, - paste0("* ", ui_paths) + ui_paths <- usethis_map_cli(paths, template = '{.path {pth("<>")}}') + file_hint <- "{cli::qty(n)}There {?is/are} {n} uncommitted file{?s}:" + ui_bullets(c( + "i" = file_hint, + bulletize(ui_paths, n_show = 10) )) # Only push if no remote & a single change push <- push && git_can_push(max_local = 1) - msg <- paste0( - "Is it ok to commit ", - if (push) "and push ", - if (n == 1) 'it' else 'them', - "?" - ) - if (ui_yeah(msg)) { + if (ui_yep(c( + "!" = "Is it ok to commit {if (push) 'and push '} {cli::qty(n)} {?it/them}?" + ))) { git_commit(paths, message) if (push) { git_push() @@ -205,10 +195,13 @@ challenge_uncommitted_changes <- function(untracked = FALSE, msg = NULL) { we push, pull, switch, or compare branches" msg <- glue(msg %||% default_msg) if (git_uncommitted(untracked = untracked)) { - if (ui_yeah("{msg}\nDo you want to proceed anyway?")) { + if (ui_yep(c( + "!" = msg, + " " = "Do you want to proceed anyway?" + ))) { return(invisible()) } else { - ui_stop("Uncommitted changes. Please commit before continuing.") + ui_abort("Uncommitted changes. Please commit before continuing.") } } } @@ -221,26 +214,32 @@ git_conflict_report <- function() { return(invisible()) } - conflicted_paths <- map_chr(conflicted, ui_path) - ui_line(c( - "There are {n} conflicted files:", - paste0("* ", conflicted_paths) + conflicted_paths <- usethis_map_cli( + conflicted, + template = '{.path {pth("<>")}}' + ) + file_hint <- "{cli::qty(n)}There {?is/are} {n} conflicted file{?s}:" + ui_bullets(c( + "i" = file_hint, + bulletize(conflicted_paths, n_show = 10) )) - msg <- glue(" - Are you ready to sort this out? - If so, we will open the conflicted files for you to edit.") + msg <- c( + "!" = "Are you ready to sort this out?", + " " = "If so, we will open the conflicted files for you to edit." + ) yes <- "Yes, I'm ready to resolve the merge conflicts." no <- "No, I want to abort this merge." - if (ui_yeah(msg, yes = yes, no = no, shuffle = FALSE)) { + if (ui_yep(msg, yes = yes, no = no, shuffle = FALSE)) { ui_silence(purrr::walk(conflicted, edit_file)) - ui_stop(" - Please fix each conflict, save, stage, and commit. - To back out of this merge, run {ui_code('gert::git_merge_abort()')} \\ - (in R) or {ui_code('git merge --abort')} (in the shell).") + ui_abort(c( + "Please fix each conflict, save, stage, and commit.", + "To back out of this merge, run {.code gert::git_merge_abort()} + (in R) or {.code git merge --abort} (in the shell)." + )) } else { gert::git_merge_abort(repo = git_repo()) - ui_stop("Abandoning the merge, since it will cause merge conflicts") + ui_abort("Abandoning the merge, since it will cause merge conflicts.") } } @@ -265,12 +264,12 @@ git_pull <- function(remref = NULL, verbose = TRUE) { remref <- remref %||% git_branch_tracking(branch) if (is.na(remref)) { if (verbose) { - ui_done("No remote branch to pull from for {ui_value(branch)}.") + ui_bullets(c("v" = "No remote branch to pull from for {.val {branch}}.")) } return(invisible()) } if (verbose) { - ui_done("Pulling from {ui_value(remref)}.") + ui_bullets(c("v" = "Pulling from {.val {remref}}.")) } gert::git_fetch( remote = remref_remote(remref), @@ -301,10 +300,10 @@ git_branch <- function() { info <- gert::git_info(repo = git_repo()) branch <- info$shorthand if (identical(branch, "HEAD")) { - ui_stop("Detached head; can't continue") + ui_abort("Detached head; can't continue.") } if (is.na(branch)) { - ui_stop("On an unborn branch -- do you need to make an initial commit?") + ui_abort("On an unborn branch -- do you need to make an initial commit?") } branch } @@ -312,7 +311,7 @@ git_branch <- function() { git_branch_tracking <- function(branch = git_branch()) { repo <- git_repo() if (!gert::git_branch_exists(branch, local = TRUE, repo = repo)) { - ui_stop("There is no local branch named {ui_value(branch)}") + ui_abort("There is no local branch named {.val {branch}}.") } gbl <- gert::git_branch_list(local = TRUE, repo = repo) sub("^refs/remotes/", "", gbl$upstream[gbl$name == branch]) @@ -342,7 +341,9 @@ git_can_push <- function(max_local = Inf, branch = git_branch(), remref = NULL) git_push <- function(branch = git_branch(), remref = NULL, verbose = TRUE) { remref <- remref %||% git_branch_tracking(branch) if (verbose) { - ui_done("Pushing local {ui_value(branch)} branch to {ui_value(remref)}.") + ui_bullets(c( + "v" = "Pushing local {.val {branch}} branch to {.val {remref}}." + )) } gert::git_push( @@ -356,10 +357,10 @@ git_push <- function(branch = git_branch(), remref = NULL, verbose = TRUE) { git_push_first <- function(branch = git_branch(), remote = "origin", verbose = TRUE) { if (verbose) { remref <- glue("{remote}/{branch}") - ui_done(" - Pushing {ui_value(branch)} branch to GitHub and setting \\ - {ui_value(remref)} as upstream branch" - ) + ui_bullets(c( + "v" = "Pushing {.val {branch}} branch to GitHub and setting + {.val {remref}} as upstream branch." + )) } gert::git_push( remote = remote, @@ -371,7 +372,8 @@ git_push_first <- function(branch = git_branch(), remote = "origin", verbose = T # Checks ------------------------------------------------------------------ -check_current_branch <- function(is = NULL, is_not = NULL, +check_current_branch <- function(is = NULL, + is_not = NULL, message = NULL) { gb <- git_branch() @@ -380,9 +382,10 @@ check_current_branch <- function(is = NULL, is_not = NULL, if (gb == is) { return(invisible()) } else { - msg <- message %||% - "Must be on branch {ui_value(is)}, not {ui_value(gb)}." - ui_stop(msg) + if (is.null(message)) { + message <- c("x" = "Must be on branch {.val {is}}, not {.val {gb}}.") + } + ui_abort(message) } } @@ -391,9 +394,10 @@ check_current_branch <- function(is = NULL, is_not = NULL, if (gb != is_not) { return(invisible()) } else { - msg <- message %||% - "Can't be on branch {ui_value(gb)}." - ui_stop(msg) + if (is.null(message)) { + message <- c("x" = "Can't be on branch {.val {gb}}.") + } + ui_abort(message) } } @@ -410,41 +414,46 @@ check_branch_up_to_date <- function(direction = c("pull", "push"), use <- use %||% switch(direction, pull = "git pull", push = "git push") if (is.na(remref)) { - ui_done("Local branch {ui_value(branch)} is not tracking a remote branch.") + ui_bullets(c( + "i" = "Local branch {.val {branch}} is not tracking a remote branch." + )) return(invisible()) } if (direction == "pull") { - ui_done(" - Checking that local branch {ui_value(branch)} has the changes \\ - in {ui_value(remref)}") + ui_bullets(c( + "v" = "Checking that local branch {.val {branch}} has the changes + in {.val {remref}}." + )) } else { - ui_done(" - Checking that remote branch {ui_value(remref)} has the changes \\ - in {ui_value(branch)}") + ui_bullets(c( + "v" = "Checking that remote branch {.val {remref}} has the changes + in {.val {branch}}." + )) } comparison <- git_branch_compare(branch, remref) - # TODO: properly pluralize "commit(s)" when I switch to cli if (direction == "pull") { if (comparison$remote_only == 0) { return(invisible()) } else { - ui_stop(" - Local branch {ui_value(branch)} is behind {ui_value(remref)} by \\ - {comparison$remote_only} commit(s). - Please use {ui_code(use)} to update.") + ui_abort(c( + "Local branch {.val {branch}} is behind {.val {remref}} by + {comparison$remote_only} commit{?s}.", + "Please use {.code {use}} to update." + )) } } else { if (comparison$local_only == 0) { return(invisible()) } else { # TODO: consider offering to push for them? - ui_stop(" - Local branch {ui_value(branch)} is ahead of {ui_value(remref)} by \\ - {comparison$local_only} commit(s). - Please use {ui_code(use)} to update.") + ui_abort(c( + "Local branch {.val {branch}} is ahead of {.val {remref}} by + {comparison$remote_only} commit{?s}.", + "Please use {.code {use}} to update." + )) } } } diff --git a/R/utils-github.R b/R/utils-github.R index ed53766af..4630448e5 100644 --- a/R/utils-github.R +++ b/R/utils-github.R @@ -2,7 +2,7 @@ parse_repo_spec <- function(repo_spec) { repo_split <- strsplit(repo_spec, "/")[[1]] if (length(repo_split) != 2) { - ui_stop("{ui_code('repo_spec')} must be of form {ui_value('owner/repo')}.") + ui_abort("{.arg repo_spec} must be of the form {.val owner/repo}.") } list(owner = repo_split[[1]], repo = repo_split[[2]]) } @@ -77,7 +77,7 @@ parse_repo_url <- function(x) { dat <- parse_github_remotes(x) # TODO: generalize here for GHE hosts that don't include 'github' if (!grepl("github", dat$host)) { - ui_stop("URL doesn't seem to be associated with GitHub: {ui_value(x)}") + ui_abort("URL doesn't seem to be associated with GitHub: {.val {x}}") } list( repo_spec = make_spec(owner = dat$repo_owner, repo = dat$repo_name), @@ -188,12 +188,13 @@ github_remotes <- function(these = c("origin", "upstream"), oops <- which(!grl$github_got) oops_remotes <- grl$remote[oops] oops_hosts <- unique(grl$host[oops]) - ui_stop(" - Unable to get GitHub info for these remotes: {ui_value(oops_remotes)} - Are we offline? Is GitHub down? Has the repo been deleted? - Otherwise, you probably need to configure a personal access token (PAT) \\ - for {ui_value(oops_hosts)} - See {ui_code('?gh_token_help')} for advice") + ui_abort(c( + "Unable to get GitHub info for these remotes: {.val {oops_remotes}}.", + "Are we offline? Is GitHub down? Has the repo been deleted?", + "Otherwise, you probably need to configure a personal access token (PAT) + for {.val {oops_hosts}}.", + "See {.run usethis::gh_token_help()} for advice." + )) } grl$default_branch <- map_chr(repo_info, "default_branch", .default = NA) @@ -327,18 +328,20 @@ github_remote_config <- function(github_get = NA) { if (!single_remote) { if (length(unique(grl$host)) != 1) { - ui_stop(" - Internal error: Multiple GitHub hosts - {ui_value(grl$host)}") + ui_abort(c( + "Internal error: Multiple GitHub hosts.", + "{.val {grl$host}}" + )) } if (length(unique(grl$github_got)) != 1) { - ui_stop(" - Internal error: Got GitHub API info for some remotes, but not all - Do all the remotes still exist? Do you still have access?") + ui_abort(c( + "Internal error: Got GitHub API info for some remotes, but not all.", + "Do all the remotes still exist? Do you still have access?" + )) } if (length(unique(grl$perm_known)) != 1) { - ui_stop(" - Internal error: Know GitHub permissions for some remotes, but not all") + ui_abort(" + Internal error: Know GitHub permissions for some remotes, but not all.") } } cfg$host_url <- unique(grl$host_url) @@ -495,11 +498,12 @@ target_repo <- function(cfg = NULL, } choices <- c( - origin = glue("{cfg$origin$repo_spec} = {ui_value('origin')}"), - upstream = glue("{cfg$upstream$repo_spec} = {ui_value('upstream')}") + origin = ui_pre_glue("<> = {.val origin}"), + upstream = ui_pre_glue("<> = {.val upstream}") ) - title <- glue("Which repo should we target?") - choice <- utils::menu(choices, graphics = FALSE, title = title) + choices_formatted <- map_chr(choices, cli::format_inline) + title <- "Which repo should we target?" + choice <- utils::menu(choices_formatted, graphics = FALSE, title = title) cfg[[names(choices)[choice]]] } @@ -510,12 +514,12 @@ target_repo_spec <- function(role = c("source", "primary"), } # formatting github remote configurations for humans --------------------------- -format_remote <- function(remote) { +pre_format_remote <- function(remote) { effective_spec <- function(remote) { if (remote$is_configured) { - ui_value(remote$repo_spec) + ui_pre_glue("{.val <>}") } else { - ui_unset("not configured") + ui_special("not configured") } } push_clause <- function(remote) { @@ -528,35 +532,42 @@ format_remote <- function(remote) { glue("{remote$name} = {effective_spec(remote)}"), push_clause(remote), if (isTRUE(remote$is_fork)) { - glue(" = fork of {ui_value(remote$parent_repo_spec)}") + ui_pre_glue(" = fork of {.val <>}") } ) glue_collapse(out) } -format_fields <- function(cfg) { +pre_format_fields <- function(cfg) { list( - type = glue("Type = {ui_value(cfg$type)}"), - host_url = glue("Host = {ui_value(cfg$host_url)}"), - pr_ready = glue("Config supports a pull request = {ui_value(cfg$pr_ready)}"), - origin = format_remote(cfg$origin), - upstream = format_remote(cfg$upstream), - desc = if (is.na(cfg$desc)) { - glue("Desc = {ui_unset('no description')}") - } else { - glue("Desc = {cfg$desc}") - } + type = ui_pre_glue("Type = {.val <>}"), + host_url = ui_pre_glue("Host = {.val <>}"), + # extra brackets here ensure value is formatted as logical (vs string) + pr_ready = ui_pre_glue("Config supports a pull request = {.val {<>}}"), + origin = pre_format_remote(cfg$origin), + upstream = pre_format_remote(cfg$upstream), + desc = cfg$desc ) } #' @export format.github_remote_config <- function(x, ...) { - glue::as_glue(format_fields(x)) + x_fmt <- pre_format_fields(x) + x_fmt$desc <- map_chr(x_fmt$desc, cli::format_inline) + x_fmt <- purrr::map_if(x_fmt, function(x) length(x) == 1, cli::format_inline) + out <- unlist(unname(x_fmt)) + + nms <- names2(out) + nms <- ifelse(nzchar(nms), nms, "*") + names(out) <- nms + + out } #' @export print.github_remote_config <- function(x, ...) { - cat(format(x, ...), sep = "\n") + withr::local_options(usethis.quiet = FALSE) + ui_bullets(format(x, ...)) invisible(x) } @@ -565,17 +576,23 @@ print.github_remote_config <- function(x, ...) { github_remote_config_wat <- function(cfg, context = c("menu", "abort")) { context <- match.arg(context) adjective <- switch(context, menu = "Unexpected", abort = "Unsupported") - out <- format_fields(cfg) - out$pr_ready <- NULL - out$type <- glue("{adjective} GitHub remote configuration: {ui_value(cfg$type)}") - out$desc <- if (is.na(cfg$desc)) NULL else cfg$desc - out + out <- format(cfg) + + type_idx <- grep("^Type", out) + out[type_idx] <- ui_pre_glue(" + <> GitHub remote configuration: {.val <>}") + names(out)[type_idx] <- "x" + + pr_idx <- grep("pull request", out) + out <- out[-pr_idx] + + unlist(out) } # returns TRUE if user selects "no" --> exit the calling function # return FALSE if user select "yes" --> keep going, they've been warned ui_github_remote_config_wat <- function(cfg) { - ui_nope( + ui_nah( github_remote_config_wat(cfg, context = "menu"), yes = "Yes, I want to proceed. I know what I'm doing.", no = "No, I want to stop and straighten out my GitHub remotes first.", @@ -584,24 +601,28 @@ ui_github_remote_config_wat <- function(cfg) { } stop_bad_github_remote_config <- function(cfg) { - abort( - message = unname(unlist(github_remote_config_wat(cfg, context = "abort"))), - class = c("usethis_error_bad_github_remote_config", "usethis_error"), + ui_abort( + github_remote_config_wat(cfg, context = "abort"), + class = "usethis_error_bad_github_remote_config", cfg = cfg ) } stop_maybe_github_remote_config <- function(cfg) { - msg <- github_remote_config_wat(cfg) - msg$type <- glue(" - Pull request functions can't work with GitHub remote configuration: \\ - {ui_value(cfg$type)} - The most likely problem is that we aren't discovering your GitHub \\ - personal access token - Call {ui_code('gh_token_help()')} for help") - abort( - message = unname(unlist(msg)), - class = c("usethis_error_invalid_pr_config", "usethis_error"), + msg <- c( + ui_pre_glue(" + Pull request functions can't work with GitHub remote configuration: + {.val <>}."), + "The most likely problem is that we aren't discovering your GitHub personal + access token.", + github_remote_config_wat(cfg) + ) + idx <- grep("Unexpected GitHub remote configuration", msg) + msg <- msg[-idx] + + ui_abort( + message = unlist(msg), + class = "usethis_error_invalid_pr_config", cfg = cfg ) } @@ -643,8 +664,8 @@ check_for_config <- function(cfg = NULL, check_for_bad_config(cfg, bad_configs = bad_configs) - ui_stop(" - Internal error: Unexpected GitHub remote configuration: {ui_value(cfg$type)}") + ui_abort(" + Internal error: Unexpected GitHub remote configuration: {.val {cfg$type}}.") } check_can_push <- function(tr = target_repo(github_get = TRUE), @@ -652,8 +673,8 @@ check_can_push <- function(tr = target_repo(github_get = TRUE), if (isTRUE(tr$can_push)) { return(invisible()) } - ui_stop(" - You don't seem to have push access for {ui_value(tr$repo_spec)}, which \\ + ui_abort(" + You don't seem to have push access for {.val {tr$repo_spec}}, which is required {objective}.") } @@ -673,15 +694,17 @@ all_configs <- function() { } read_more <- function() { - glue(" - Read more about the GitHub remote configurations that usethis supports at: - {ui_value('https://happygitwithr.com/common-remote-setups.html')}") + c( + "i" = "Read more about the GitHub remote configurations that usethis supports at:", + " " = "{.url https://happygitwithr.com/common-remote-setups.html}." + ) } read_more_maybe <- function() { - glue(" - Read more about what this GitHub remote configurations means at: - {ui_value('https://happygitwithr.com/common-remote-setups.html')}") + c( + "i" = "Read more about what this GitHub remote configuration means at:", + " " = "{.url https://happygitwithr.com/common-remote-setups.html}." + ) } cfg_no_github <- function(cfg) { @@ -690,11 +713,10 @@ cfg_no_github <- function(cfg) { list( type = "no_github", pr_ready = FALSE, - desc = glue(" - Neither {ui_value('origin')} nor {ui_value('upstream')} is a GitHub \\ - repo. - - {read_more()}") + desc = c( + "!" = "Neither {.val origin} nor {.val upstream} is a GitHub repo.", + read_more() + ) ) ) } @@ -705,10 +727,10 @@ cfg_ours <- function(cfg) { list( type = "ours", pr_ready = TRUE, - desc = glue(" - {ui_value('origin')} is both the source and primary repo. - - {read_more()}") + desc = c( + "i" = "{.val origin} is both the source and primary repo.", + read_more() + ) ) ) } @@ -720,13 +742,14 @@ cfg_theirs <- function(cfg) { list( type = "theirs", pr_ready = FALSE, - desc = glue(" - The only configured GitHub remote is {ui_value(configured)}, which - you cannot push to. - If your goal is to make a pull request, you must fork-and-clone. - {ui_code('usethis::create_from_github()')} can do this. - - {read_more()}") + desc = c( + "!" = ui_pre_glue(" + The only configured GitHub remote is {.val <>}, + which you cannot push to."), + "i" = "If your goal is to make a pull request, you must fork-and-clone.", + "i" = "{.fun usethis::create_from_github} can do this.", + read_more() + ) ) ) } @@ -744,14 +767,16 @@ cfg_maybe_ours_or_theirs <- function(cfg) { list( type = "maybe_ours_or_theirs", pr_ready = NA, - desc = glue(" - {ui_value(configured)} is a GitHub repo and {ui_value(not_configured)} \\ - is either not configured or is not a GitHub repo. - - We may be offline or you may need to configure a GitHub personal access - token. {ui_code('gh_token_help()')} can help with that. - - {read_more_maybe()}") + desc = c( + "!" = ui_pre_glue(" + {.val <>} is a GitHub repo and + {.val <>} is either not configured or is not a + GitHub repo."), + "i" = "We may be offline or you may need to configure a GitHub personal + access token.", + "i" = "{.run usethis::gh_token_help()} can help with that.", + read_more_maybe() + ) ) ) } @@ -762,11 +787,12 @@ cfg_fork <- function(cfg) { list( type = "fork", pr_ready = TRUE, - desc = glue(" - {ui_value('origin')} is a fork of {ui_value(cfg$upstream$repo_spec)}, \\ - which is configured as the {ui_value('upstream')} remote. - - {read_more()}") + desc = c( + "i" = ui_pre_glue(" + {.val origin} is a fork of {.val <>}, + which is configured as the {.val upstream} remote."), + read_more() + ) ) ) } @@ -777,16 +803,17 @@ cfg_maybe_fork <- function(cfg) { list( type = "maybe_fork", pr_ready = NA, - desc = glue(" - Both {ui_value('origin')} and {ui_value('upstream')} appear to be \\ - GitHub repos. However, we can't confirm their relationship to each \\ - other (e.g., fork and fork parent) or your permissions (e.g. push \\ - access). - - We may be offline or you may need to configure a GitHub personal access - token. {ui_code('gh_token_help()')} can help with that. - - {read_more_maybe()}") + desc = c( + "!" = ui_pre_glue(" + Both {.val origin} and {.val upstream} appear to be GitHub + repos. However, we can't confirm their relationship to each + other (e.g., fork and fork parent) or your permissions (e.g. + push access)."), + "i" = "We may be offline or you may need to configure a GitHub personal + access token.", + "i" = "{.run usethis::gh_token_help()} can help with that.", + read_more_maybe() + ) ) ) } @@ -797,10 +824,11 @@ cfg_fork_cannot_push_origin <- function(cfg) { list( type = "fork_cannot_push_origin", pr_ready = FALSE, - desc = glue(" - The {ui_value('origin')} remote is a fork, but you can't push to it. - - {read_more()}") + desc = c( + "!" = ui_pre_glue(" + The {.val origin} remote is a fork, but you can't push to it."), + read_more() + ) ) ) } @@ -811,11 +839,12 @@ cfg_fork_upstream_is_not_origin_parent <- function(cfg) { list( type = "fork_upstream_is_not_origin_parent", pr_ready = FALSE, - desc = glue(" - The {ui_value('origin')} GitHub remote is a fork, but its parent is \\ - not configured as the {ui_value('upstream')} remote. - - {read_more()}") + desc = c( + "!" = ui_pre_glue(" + The {.val origin} GitHub remote is a fork, but its parent is + not configured as the {.val upstream} remote."), + read_more() + ) ) ) } @@ -826,12 +855,173 @@ cfg_upstream_but_origin_is_not_fork <- function(cfg) { list( type = "upstream_but_origin_is_not_fork", pr_ready = FALSE, - desc = glue(" - Both {ui_value('origin')} and {ui_value('upstream')} are GitHub \\ - remotes, but {ui_value('origin')} is not a fork and, in particular, \\ - is not a fork of {ui_value('upstream')}. - - {read_more()}") + desc = c( + "!" = ui_pre_glue(" + Both {.val origin} and {.val upstream} are GitHub remotes, but + {.val origin} is not a fork and, in particular, is not a fork of + {.val upstream}."), + read_more() + ) ) ) } + +# construct instances of `github_remote_config` for dev/testing purposes-------- +new_no_github <- function() { + cfg <- new_github_remote_config() + cfg_no_github(cfg) +} + +new_ours <- function() { + remotes <- data.frame(name = "origin", url = "https://github.com/OWNER/REPO.git") + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <- grl$perm_known <- TRUE + grl$default_branch <- "DEFAULT_BRANCH" + + grl$is_fork <- FALSE + grl$can_push <- grl$can_admin <- TRUE + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$host_url <- grl$host_url + cfg$origin$is_configured <- TRUE + cfg_ours(cfg) +} + +new_theirs <- function() { + remotes <- data.frame(name = "origin", url = "https://github.com/OWNER/REPO.git") + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <- grl$perm_known <- TRUE + grl$default_branch <- "DEFAULT_BRANCH" + + grl$can_push <- grl$can_admin <- FALSE + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$host_url <- grl$host_url + cfg$origin$is_configured <- TRUE + cfg_theirs(cfg) +} + +new_fork <- function() { + remotes <- data.frame( + name = c("origin", "upstream"), + url = c("https://github.com/CONTRIBUTOR/REPO.git", "https://github.com/OWNER/REPO.git") + ) + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <- grl$perm_known <- TRUE + grl$default_branch <- "DEFAULT_BRANCH" + + grl$is_fork <- c(TRUE, FALSE) + grl$parent_repo_owner <- c("OWNER", NA) + grl$parent_repo_name <- c("REPO", NA) + grl$can_push_to_parent <- c(FALSE, NA) + grl$parent_repo_spec <- make_spec(grl$parent_repo_owner, grl$parent_repo_name) + + grl$can_push <- grl$can_admin <- c(TRUE, FALSE) + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$upstream <- utils::modifyList(cfg$upstream, grl[grl$remote == "upstream",]) + cfg$host_url <- grl$host_url[1] + cfg$origin$is_configured <- cfg$upstream$is_configured <- TRUE + cfg$origin$parent_is_upstream <- TRUE + cfg_fork(cfg) +} + +new_maybe_ours_or_theirs <- function() { + remotes <- data.frame(name = "origin", url = "https://github.com/OWNER/REPO.git") + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <-grl$perm_known <- FALSE + grl$default_branch <- "DEFAULT_BRANCH" + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$host_url <- grl$host_url + cfg$origin$is_configured <- TRUE + cfg_maybe_ours_or_theirs(cfg) +} + +new_maybe_fork <- function() { + remotes <- data.frame( + name = c("origin", "upstream"), + url = c("https://github.com/CONTRIBUTOR/REPO.git", "https://github.com/OWNER/REPO.git") + ) + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <-grl$perm_known <- FALSE + grl$default_branch <- "DEFAULT_BRANCH" + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$upstream <- utils::modifyList(cfg$upstream, grl[grl$remote == "upstream",]) + cfg$host_url <- grl$host_url[1] + cfg$origin$is_configured <- cfg$upstream$is_configured <- TRUE + cfg_maybe_fork(cfg) +} + +new_fork_cannot_push_origin <- function() { + remotes <- data.frame( + name = c("origin", "upstream"), + url = c("https://github.com/CONTRIBUTOR/REPO.git", "https://github.com/OWNER/REPO.git") + ) + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <-grl$perm_known <- TRUE + grl$default_branch <- "DEFAULT_BRANCH" + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$upstream <- utils::modifyList(cfg$upstream, grl[grl$remote == "upstream",]) + cfg$host_url <- grl$host_url[1] + cfg$origin$is_configured <- cfg$upstream$is_configured <- TRUE + + cfg$origin$parent_is_upstream <- FALSE + + cfg_fork_cannot_push_origin(cfg) +} + +new_fork_upstream_is_not_origin_parent<- function() { + remotes <- data.frame( + name = c("origin", "upstream"), + url = c("https://github.com/CONTRIBUTOR/REPO.git", "https://github.com/OLD_OWNER/REPO.git") + ) + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <- grl$perm_known <- TRUE + grl$default_branch <- "DEFAULT_BRANCH" + + grl$is_fork <- c(TRUE, FALSE) + grl$parent_repo_owner <- c("NEW_OWNER", NA) + grl$parent_repo_name <- c("REPO", NA) + grl$can_push_to_parent <- c(FALSE, NA) + grl$parent_repo_spec <- make_spec(grl$parent_repo_owner, grl$parent_repo_name) + grl$can_push <- grl$can_admin <- c(TRUE, FALSE) + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$upstream <- utils::modifyList(cfg$upstream, grl[grl$remote == "upstream",]) + cfg$host_url <- grl$host_url[1] + cfg$origin$is_configured <- cfg$upstream$is_configured <- TRUE + + cfg_fork_upstream_is_not_origin_parent(cfg) +} + +new_upstream_but_origin_is_not_fork <- function() { + remotes <- data.frame( + name = c("origin", "upstream"), + url = c("https://github.com/CONTRIBUTOR/REPO.git", "https://github.com/OWNER/REPO.git") + ) + grl <- github_remotes(github_get = FALSE, x = remotes) + grl$github_got <-grl$perm_known <- TRUE + grl$default_branch <- "DEFAULT_BRANCH" + + grl$is_fork <- FALSE + + cfg <- new_github_remote_config() + cfg$origin <- utils::modifyList(cfg$origin, grl[grl$remote == "origin",]) + cfg$upstream <- utils::modifyList(cfg$upstream, grl[grl$remote == "upstream",]) + cfg$host_url <- grl$host_url[1] + cfg$origin$is_configured <- cfg$upstream$is_configured <- TRUE + + cfg$origin$parent_is_upstream <- FALSE + + cfg_upstream_but_origin_is_not_fork(cfg) +} diff --git a/R/utils-ui.R b/R/utils-ui.R index e57d7c14f..fa0a9b187 100644 --- a/R/utils-ui.R +++ b/R/utils-ui.R @@ -1,8 +1,219 @@ -# opening act of an eventual transition away from the ui_*() functions and towards -# the cli-mediated UI we're using in other packages +# usethis theme ---------------------------------------------------------------- +usethis_theme <- function() { + list( + # add a "todo" bullet, which is intended to be seen as an unchecked checkbox + ".bullets .bullet-_" = list( + "text-exdent" = 2, + before = function(x) paste0(cli::col_red(cli::symbol$checkbox_off), " ") + ), + # historically, usethis has used yellow for this + ".bullets .bullet-i" = list( + "text-exdent" = 2, + before = function(x) paste0(cli::col_yellow(cli::symbol$info), " ") + ), + # we have enough color going on already, don't add color to `*` bullets + ".bullets .bullet-*" = list( + "text-exdent" = 2, + before = function(x) paste0(cli::symbol$bullet, " ") + ), + # apply quotes to `.field` if we can't style it with color + span.field = list(transform = single_quote_if_no_color) + ) +} + +single_quote_if_no_color <- function(x) quote_if_no_color(x, "'") + +quote_if_no_color <- function(x, quote = "'") { + # copied from googledrive + # TODO: if a better way appears in cli, use it + # @gabor says: "if you want to have before and after for the no-color case + # only, we can have a selector for that, such as: + # span.field::no-color + # (but, at the time I write this, cli does not support this yet) + if (cli::num_ansi_colors() > 1) { + x + } else { + paste0(quote, x, quote) + } +} + +# silence ----------------------------------------------------------------- +#' Suppress usethis's messaging +#' +#' Execute a bit of code without usethis's normal messaging. +#' +#' @param code Code to execute with usual UI output silenced. +#' +#' @returns Whatever `code` returns. +#' @export +#' @examples +#' # compare the messaging you see from this: +#' browse_github("usethis") +#' # vs. this: +#' ui_silence( +#' browse_github("usethis") +#' ) +ui_silence <- function(code) { + withr::with_options(list(usethis.quiet = TRUE), code) +} + +is_quiet <- function() { + isTRUE(getOption("usethis.quiet", default = FALSE)) +} + +# bullets, helpers, and friends ------------------------------------------------ +ui_bullets <- function(text, .envir = parent.frame()) { + if (is_quiet()) { + return(invisible()) + } + cli::cli_div(theme = usethis_theme()) + cli::cli_bullets(text, .envir = .envir) +} + +ui_path_impl <- function(x, base = NULL) { + is_directory <- is_dir(x) | grepl("/$", x) + if (is.null(base)) { + x <- proj_rel_path(x) + } else if (!identical(base, NA)) { + x <- path_rel(x, base) + } + + # rationalize trailing slashes + x <- path_tidy(x) + x[is_directory] <- paste0(x[is_directory], "/") + + unclass(x) +} + +# shorter form for compactness, because this is typical usage: +# ui_bullets("blah blah {.path {pth(some_path)}}") +pth <- ui_path_impl + +ui_code_snippet <- function(x, + copy = rlang::is_interactive(), + language = c("R", ""), + interpolate = TRUE, + .envir = parent.frame()) { + language <- arg_match(language) + + indent <- function(x, first = " ", indent = first) { + x <- gsub("\n", paste0("\n", indent), x) + paste0(first, x) + } + + x <- glue_collapse(x, "\n") + if (interpolate) { + x <- glue(x, .envir = .envir) + # what about literal `{` or `}`? + # use `interpolate = FALSE`, if appropriate + # double them, i.e. `{{` or `}}` + # open issue/PR about adding `.open` and `.close` + } + + if (!is_quiet()) { + # the inclusion of `.envir = .envir` leads to test failure + # I'm consulting with Gabor on this + # leaving it out seems fine for my use case + # cli::cli_code(indent(x), language = language, .envir = .envir) + cli::cli_code(indent(x), language = language) + } + + if (copy && clipr::clipr_available()) { + x_no_ansi <- cli::ansi_strip(x) + clipr::write_clip(x_no_ansi) + style_subtle <- cli::combine_ansi_styles( + cli::make_ansi_style("grey"), + cli::style_italic + ) + ui_bullets(c(" " = style_subtle("[Copied to clipboard]"))) + } + + invisible(x) +} + +# inspired by gargle::gargle_map_cli() and gargle::bulletize() +usethis_map_cli <- function(x, ...) UseMethod("usethis_map_cli") + +#' @export +usethis_map_cli.default <- function(x, ...) { + ui_abort(c( + "x" = "Don't know how to {.fun usethis_map_cli} an object of class + {.obj_type_friendly {x}}." + )) +} + +#' @export +usethis_map_cli.NULL <- function(x, ...) NULL + +#' @export +usethis_map_cli.character <- function(x, + template = "{.val <>}", + .open = "<<", .close = ">>", + ...) { + as.character(glue(template, .open = .open, .close = .close)) +} + +ui_pre_glue <- function(..., .envir = parent.frame()) { + glue(..., .open = "<<", .close = ">>", .envir = .envir) +} + +bulletize <- function(x, bullet = "*", n_show = 5, n_fudge = 2) { + n <- length(x) + n_show_actual <- compute_n_show(n, n_show, n_fudge) + out <- utils::head(x, n_show_actual) + n_not_shown <- n - n_show_actual + + out <- set_names(out, rep_along(out, bullet)) + + if (n_not_shown == 0) { + out + } else { + c(out, " " = glue("{cli::symbol$ellipsis} and {n_not_shown} more")) + } +} + +# I don't want to do "... and x more" if x is silly, i.e. 1 or 2 +compute_n_show <- function(n, n_show_nominal = 5, n_fudge = 2) { + if (n > n_show_nominal && n - n_show_nominal > n_fudge) { + n_show_nominal + } else { + n + } +} + +kv_line <- function(key, value, .envir = parent.frame()) { + cli::cli_div(theme = usethis_theme()) + + key_fmt <- cli::format_inline(key, .envir = .envir) + + # this must happen first, before `value` has been forced + value_fmt <- cli::format_inline("{.val {value}}") + # but we might actually want something other than value_fmt + if (is.null(value)) { + value <- ui_special() + } + if (inherits(value, "AsIs")) { + value_fmt <- cli::format_inline(value, .envir = .envir) + } + + ui_bullets(c("*" = "{key_fmt}: {value_fmt}")) +} + +ui_special <- function(x = "unset") { + force(x) + I(glue("{cli::col_grey('<[x]>')}", .open = "[", .close = "]")) +} + +# errors ----------------------------------------------------------------------- +ui_abort <- function(message, ..., class = NULL, .envir = parent.frame()) { + cli::cli_div(theme = usethis_theme()) + + nms <- names2(message) + default_nms <- rep_along(message, "i") + default_nms[1] <- "x" + nms <- ifelse(nzchar(nms), nms, default_nms) + names(message) <- nms -usethis_abort <- function(message, ..., class = NULL, .envir = parent.frame()) { - #cli::cli_div(theme = usethis_theme()) cli::cli_abort( message, class = c(class, "usethis_error"), @@ -10,3 +221,44 @@ usethis_abort <- function(message, ..., class = NULL, .envir = parent.frame()) { ... ) } + +# questions -------------------------------------------------------------------- +ui_yep <- function(x, + yes = c("Yes", "Definitely", "For sure", "Yup", "Yeah", "I agree", "Absolutely"), + no = c("No way", "Not now", "Negative", "No", "Nope", "Absolutely not"), + n_yes = 1, n_no = 2, shuffle = TRUE, + .envir = parent.frame()) { + if (!is_interactive()) { + ui_abort(c( + "User input required, but session is not interactive.", + "Query: {.val {x}}" + )) + } + + n_yes <- min(n_yes, length(yes)) + n_no <- min(n_no, length(no)) + + qs <- c(sample(yes, n_yes), sample(no, n_no)) + + if (shuffle) { + qs <- sample(qs) + } + + cli::cli_inform(x, .envir = .envir) + out <- utils::menu(qs) + out != 0L && qs[[out]] %in% yes +} + +ui_nah <- function(x, + yes = c("Yes", "Definitely", "For sure", "Yup", "Yeah", "I agree", "Absolutely"), + no = c("No way", "Not now", "Negative", "No", "Nope", "Absolutely not"), + n_yes = 1, n_no = 2, shuffle = TRUE, + .envir = parent.frame()) { + # TODO(jennybc): is this correct in the case of no selection / cancelling? + !ui_yep( + x = x, yes = yes, no = no, + n_yes = n_yes, n_no = n_no, + shuffle = shuffle, + .envir = .envir + ) +} diff --git a/R/utils.R b/R/utils.R index a48c9d563..50c9846a2 100644 --- a/R/utils.R +++ b/R/utils.R @@ -16,7 +16,7 @@ can_overwrite <- function(path) { } if (is_interactive()) { - ui_yeah("Overwrite pre-existing file {ui_path(path)}?") + ui_yep(c("!" = "Overwrite pre-existing file {.path {pth(path)}}?")) } else { FALSE } @@ -24,13 +24,11 @@ can_overwrite <- function(path) { check_is_named_list <- function(x, nm = deparse(substitute(x))) { if (!is_list(x)) { - bad_class <- paste(class(x), collapse = "/") - ui_stop("{ui_code(nm)} must be a list, not {ui_value(bad_class)}.") + ui_abort("{.code {nm}} must be a list, not {.obj_type_friendly {x}}.") } if (!is_dictionaryish(x)) { - ui_stop( - "Names of {ui_code(nm)} must be non-missing, non-empty, and non-duplicated." - ) + ui_abort( + "Names of {.code {nm}} must be non-missing, non-empty, and non-duplicated.") } x } diff --git a/R/version.R b/R/version.R index 07343e882..226e3a98f 100644 --- a/R/version.R +++ b/R/version.R @@ -141,8 +141,9 @@ use_c_version <- function(ver) { } hint <- glue("{project_name()}_version") - ui_done(" - Setting {ui_field(hint)} to {ui_value(ver)} in {ui_path(version_path)}") + ui_bullets(c( + "v" = "Setting {.field {hint}} to {.val {ver}} {.path {pth(version_path)}}." + )) lines <- read_utf8(version_path) diff --git a/R/vignette.R b/R/vignette.R index 1e563dcbf..ceab26aa0 100644 --- a/R/vignette.R +++ b/R/vignette.R @@ -87,10 +87,10 @@ use_vignette_template <- function(template, name, title, subdir = NULL) { check_vignette_name <- function(name) { if (!valid_vignette_name(name)) { - ui_stop(c( - "{ui_value(name)} is not a valid filename for a vignette. It must:", - "* Start with a letter.", - "* Contain only letters, numbers, '_', and '-'." + ui_abort(c( + "{.val {name}} is not a valid filename for a vignette. It must:", + "Start with a letter.", + "Contain only letters, numbers, '_', and '-'." )) } } diff --git a/R/write.R b/R/write.R index 1a97ad3a9..e506ec563 100644 --- a/R/write.R +++ b/R/write.R @@ -60,7 +60,7 @@ write_union <- function(path, lines, quiet = FALSE) { } if (!quiet) { - ui_done("Adding {ui_value(new)} to {ui_path(path)}") + ui_bullets(c("v" = "Adding {.val {new}} to {.path {pth(path)}}.")) } all <- c(existing_lines, new) @@ -86,12 +86,12 @@ write_over <- function(path, lines, quiet = FALSE, overwrite = FALSE) { if (overwrite || can_overwrite(path)) { if (!quiet) { - ui_done("Writing {ui_path(path)}") + ui_bullets(c("v" = "Writing {.path {pth(path)}}.")) } write_utf8(path, lines) } else { if (!quiet) { - ui_done("Leaving {ui_path(path)} unchanged") + ui_bullets(c("i" = "Leaving {.path {pth(path)}} unchanged.")) } invisible(FALSE) } diff --git a/README.Rmd b/README.Rmd index 6f8d69db7..0fbb3d006 100644 --- a/README.Rmd +++ b/README.Rmd @@ -46,7 +46,7 @@ Most `use_*()` functions operate on the *active project*: literally, a directory A few usethis functions have no strong connections to projects and will expect you to provide a path. -usethis is quite chatty, explaining what it's doing and assigning you tasks. `✔` indicates something usethis has done for you. `●` indicates that you'll need to do some work yourself. +usethis is quite chatty, explaining what it's doing and assigning you tasks. `✔` indicates something usethis has done for you. `☐` indicates that you'll need to do some work yourself. Below is a quick look at how usethis can help to set up a package. But remember, many usethis functions are also applicable to analytical projects that are not packages. diff --git a/README.md b/README.md index b8c3baf3f..de29e6036 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ A few usethis functions have no strong connections to projects and will expect you to provide a path. usethis is quite chatty, explaining what it’s doing and assigning you -tasks. `✔` indicates something usethis has done for you. `●` indicates +tasks. `✔` indicates something usethis has done for you. `☐` indicates that you’ll need to do some work yourself. Below is a quick look at how usethis can help to set up a package. But @@ -61,10 +61,10 @@ library(usethis) # Create a new package ------------------------------------------------- path <- file.path(tempdir(), "mypkg") create_package(path) -#> ✔ Creating '/tmp/RtmpPqIkgo/mypkg/' -#> ✔ Setting active project to '/private/tmp/RtmpPqIkgo/mypkg' -#> ✔ Creating 'R/' -#> ✔ Writing 'DESCRIPTION' +#> ✔ Creating '/tmp/Rtmp7yJh5e/mypkg/'. +#> ✔ Setting active project to "/private/tmp/Rtmp7yJh5e/mypkg". +#> ✔ Creating 'R/'. +#> ✔ Writing 'DESCRIPTION'. #> Package: mypkg #> Title: What the Package Does (One Line, Title Case) #> Version: 0.0.0.9000 @@ -76,54 +76,56 @@ create_package(path) #> Encoding: UTF-8 #> Roxygen: list(markdown = TRUE) #> RoxygenNote: 7.3.1 -#> ✔ Writing 'NAMESPACE' -#> ✔ Setting active project to '' +#> ✔ Writing 'NAMESPACE'. +#> ✔ Setting active project to "". # only needed since this session isn't interactive proj_activate(path) -#> ✔ Setting active project to '/private/tmp/RtmpPqIkgo/mypkg' -#> ✔ Changing working directory to '/tmp/RtmpPqIkgo/mypkg/' +#> ✔ Setting active project to "/private/tmp/Rtmp7yJh5e/mypkg". +#> ✔ Changing working directory to '/tmp/Rtmp7yJh5e/mypkg/' # Modify the description ---------------------------------------------- use_mit_license("My Name") -#> ✔ Adding 'MIT + file LICENSE' to License -#> ✔ Writing 'LICENSE' -#> ✔ Writing 'LICENSE.md' -#> ✔ Adding '^LICENSE\\.md$' to '.Rbuildignore' +#> ✔ Adding "MIT + file LICENSE" to 'License'. +#> ✔ Writing 'LICENSE'. +#> ✔ Writing 'LICENSE.md'. +#> ✔ Adding "^LICENSE\\.md$" to '.Rbuildignore'. use_package("ggplot2", "Suggests") -#> ✔ Adding 'ggplot2' to Suggests field in DESCRIPTION -#> • Use `requireNamespace("ggplot2", quietly = TRUE)` to test if package is installed -#> • Then directly refer to functions with `ggplot2::fun()` +#> ✔ Adding ggplot2 to 'Suggests' field in DESCRIPTION. +#> ☐ Use `requireNamespace("ggplot2", quietly = TRUE)` to test if package is +#> installed. +#> ☐ Then directly refer to functions with `ggplot2::fun()`. # Set up other files ------------------------------------------------- use_readme_md() -#> ✔ Writing 'README.md' -#> • Update 'README.md' to include installation instructions. +#> ✔ Writing 'README.md'. +#> ☐ Update 'README.md' to include installation instructions. use_news_md() -#> ✔ Writing 'NEWS.md' +#> ✔ Writing 'NEWS.md'. use_test("my-test") -#> ✔ Adding 'testthat' to Suggests field in DESCRIPTION -#> ✔ Adding '3' to Config/testthat/edition -#> ✔ Creating 'tests/testthat/' -#> ✔ Writing 'tests/testthat.R' -#> ✔ Writing 'tests/testthat/test-my-test.R' -#> • Edit 'tests/testthat/test-my-test.R' +#> ✔ Adding testthat to 'Suggests' field in DESCRIPTION. +#> ✔ Adding "3" to 'Config/testthat/edition'. +#> ✔ Creating 'tests/testthat/'. +#> ✔ Writing 'tests/testthat.R'. +#> ✔ Writing 'tests/testthat/test-my-test.R'. +#> ☐ Edit 'tests/testthat/test-my-test.R'. x <- 1 y <- 2 use_data(x, y) -#> ✔ Adding 'R' to Depends field in DESCRIPTION -#> ✔ Creating 'data/' -#> ✔ Setting LazyData to 'true' in 'DESCRIPTION' -#> ✔ Saving 'x', 'y' to 'data/x.rda', 'data/y.rda' -#> • Document your data (see 'https://r-pkgs.org/data.html') +#> ✔ Adding R to 'Depends' field in DESCRIPTION. +#> ✔ Creating 'data/'. +#> ✔ Setting 'LazyData' to "true" in 'DESCRIPTION'. +#> ✔ Saving "x" and "y" to "data/x.rda" and "data/y.rda". +#> ☐ Document your data (see ). # Use git ------------------------------------------------------------ use_git() -#> ✔ Initialising Git repo -#> ✔ Adding '.Rproj.user', '.Rhistory', '.Rdata', '.httr-oauth', '.DS_Store', '.quarto' to '.gitignore' +#> ✔ Initialising Git repo. +#> ✔ Adding ".Rproj.user", ".Rhistory", ".Rdata", ".httr-oauth", ".DS_Store", and +#> ".quarto" to '.gitignore'. ``` ## Code of Conduct diff --git a/_pkgdown.yml b/_pkgdown.yml index 9b984ec66..11664e0a4 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -103,10 +103,11 @@ reference: - matches("tidy") - title: Configuration desc: > - Configure the behaviour of R or RStudio, globally as a user or + Configure the behaviour of R or RStudio or usethis, globally as a user or for a specific project contents: - usethis_options + - ui_silence - use_blank_slate - use_devtools - use_usethis @@ -169,3 +170,21 @@ reference: - use_github_actions - use_tidy_labels - use_travis + +articles: +- title: Basics + navbar: ~ + contents: + - articles/usethis-setup + +- title: Git and GitHub + navbar: Git and GitHub + contents: + - articles/git-credentials + - articles/pr-functions + +- title: Developer + desc: Articles about package internals and development experiments + contents: + - articles/ui-cli-conversion + - articles/badge-accessibility diff --git a/man/git_vaccinate.Rd b/man/git_vaccinate.Rd index 79f938a36..4d7a894b5 100644 --- a/man/git_vaccinate.Rd +++ b/man/git_vaccinate.Rd @@ -7,9 +7,10 @@ git_vaccinate() } \description{ -Adds \code{.Rproj.user}, \code{.Rhistory}, \code{.Rdata}, \code{.httr-oauth}, \code{.DS_Store}, and \code{.quarto} to -your global (a.k.a. user-level) \code{.gitignore}. This is good practice as it -decreases the chance that you will accidentally leak credentials to GitHub. -\code{git_vaccinate()} also tries to detect and fix the situation where you have a -global gitignore file, but it's missing from your global Git config. +Adds \code{.Rproj.user}, \code{.Rhistory}, \code{.Rdata}, \code{.httr-oauth}, \code{.DS_Store}, and +\code{.quarto} to your global (a.k.a. user-level) \code{.gitignore}. This is good +practice as it decreases the chance that you will accidentally leak +credentials to GitHub. \code{git_vaccinate()} also tries to detect and fix the +situation where you have a global gitignore file, but it's missing from your +global Git config. } diff --git a/man/ui.Rd b/man/ui-legacy-functions.Rd similarity index 68% rename from man/ui.Rd rename to man/ui-legacy-functions.Rd index 2bbe5b32d..1a1976122 100644 --- a/man/ui.Rd +++ b/man/ui-legacy-functions.Rd @@ -1,7 +1,7 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ui.R -\name{ui} -\alias{ui} +% Please edit documentation in R/ui-legacy.R +\name{ui-legacy-functions} +\alias{ui-legacy-functions} \alias{ui_line} \alias{ui_todo} \alias{ui_done} @@ -10,13 +10,12 @@ \alias{ui_code_block} \alias{ui_stop} \alias{ui_warn} -\alias{ui_silence} \alias{ui_field} \alias{ui_value} \alias{ui_path} \alias{ui_code} \alias{ui_unset} -\title{User interface} +\title{Legacy functions related to user interface} \usage{ ui_line(x = character(), .envir = parent.frame()) @@ -34,8 +33,6 @@ ui_stop(x, .envir = parent.frame()) ui_warn(x, .envir = parent.frame()) -ui_silence(code) - ui_field(x) ui_value(x) @@ -60,8 +57,6 @@ environment. For expert use only.} \item{copy}{If \code{TRUE}, the session is interactive, and the clipr package is installed, will copy the code block to the clipboard.} -\item{code}{Code to execute with usual UI output silenced.} - \item{base}{If specified, paths will be displayed relative to this path.} } \value{ @@ -69,10 +64,21 @@ The block styles, conditions, and questions are called for their side-effect. The inline styles return a string. } \description{ -These functions are used to construct the user interface of usethis. Use -them in your own package so that your \code{use_} functions work the same way -as usethis. - +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} + +These functions are now superseded. External users of the \verb{usethis::ui_*()} +functions are encouraged to use the \href{https://cli.r-lib.org/}{cli package} +instead. The cli package did not have the required functionality when the +\verb{usethis::ui_*()} functions were created, but it has had that for a while +now and it's the superior option. There is even a cli vignette about how to +make this transition: \code{vignette("usethis-ui", package = "cli")}. + +usethis itself now uses cli internally for its UI, but these new functions +are not exported and presumably never will be. There is a developer-focused +article on the process of transitioning usethis's own UI to use cli (LINK +TO COME). +} +\details{ The \code{ui_} functions can be broken down into four main categories: \itemize{ \item block styles: \code{ui_line()}, \code{ui_done()}, \code{ui_todo()}, \code{ui_oops()}, @@ -84,14 +90,11 @@ The \code{ui_} functions can be broken down into four main categories: } The question functions \code{\link[=ui_yeah]{ui_yeah()}} and \code{\link[=ui_nope]{ui_nope()}} have their own \link[=ui-questions]{help page}. -} -\section{Silencing output}{ All UI output (apart from \code{ui_yeah()}/\code{ui_nope()} prompts) can be silenced -by setting \code{options(usethis.quiet = TRUE)}. Use \code{ui_silence()} to silence +by setting \code{options(usethis.quiet = TRUE)}. Use \code{\link[=ui_silence]{ui_silence()}} to silence selected actions. } - \examples{ new_val <- "oxnard" ui_done("{ui_field('name')} set to {ui_value(new_val)}") @@ -103,9 +106,4 @@ ui_code_block(c( "Line 3" )) } -\seealso{ -Other user interface functions: -\code{\link{ui-questions}} -} -\concept{user interface functions} \keyword{internal} diff --git a/man/ui-questions.Rd b/man/ui-questions.Rd index db32d71a0..5f8982796 100644 --- a/man/ui-questions.Rd +++ b/man/ui-questions.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ui.R +% Please edit documentation in R/ui-legacy.R \name{ui-questions} \alias{ui-questions} \alias{ui_yeah} @@ -57,9 +57,12 @@ refuses to make a selection (cancels). \code{ui_nope()} is the logical opposite of \code{ui_yeah()}. } \description{ -These functions are used to interact with the user by posing a simple yes or -no question. For details on the other \verb{ui_*()} functions, see the \link{ui} help -page. +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} + +\code{ui_yeah()} and \code{ui_nope()} are technically superseded, but, unlike the rest +of the legacy \code{\link[=ui-legacy-functions]{ui_*()}} functions, there's not yet a +drop-in replacement available in the \href{https://cli.r-lib.org/}{cli package}. +\code{ui_yeah()} and \code{ui_nope()} are no longer used internally in usethis. } \examples{ \dontrun{ @@ -68,9 +71,4 @@ ui_nope("Have you tried turning it off and on again?", n_yes = 1, n_no = 1) ui_yeah("Are you sure its plugged in?", yes = "Yes", no = "No", shuffle = FALSE) } } -\seealso{ -Other user interface functions: -\code{\link{ui}} -} -\concept{user interface functions} \keyword{internal} diff --git a/man/ui_silence.Rd b/man/ui_silence.Rd new file mode 100644 index 000000000..42da92845 --- /dev/null +++ b/man/ui_silence.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-ui.R +\name{ui_silence} +\alias{ui_silence} +\title{Suppress usethis's messaging} +\usage{ +ui_silence(code) +} +\arguments{ +\item{code}{Code to execute with usual UI output silenced.} +} +\value{ +Whatever \code{code} returns. +} +\description{ +Execute a bit of code without usethis's normal messaging. +} +\examples{ +# compare the messaging you see from this: +browse_github("usethis") +# vs. this: +ui_silence( + browse_github("usethis") +) +} diff --git a/man/use_import_from.Rd b/man/use_import_from.Rd index 1c29d105c..63d1e78cc 100644 --- a/man/use_import_from.Rd +++ b/man/use_import_from.Rd @@ -29,6 +29,6 @@ session. } \examples{ \dontrun{ -use_import_from("usethis", "ui_todo") +use_import_from("glue", "glue") } } diff --git a/principles.md b/principles.md index af74dc73d..6a76fe5d9 100644 --- a/principles.md +++ b/principles.md @@ -81,14 +81,18 @@ In order to be consistent everywhere, all paths supplied by the user should be p ## Communicating with the user -User-facing messages are emitted via helpers in `ui.R` and *everything* is eventually routed through `rlang::inform()` via `ui_inform()`. +User-facing messages are ALWAYS emitted via helpers in `utils-ui.R`. This is all intentional and should be preserved. This is so we can control verbosity package-wide with the `usethis.quiet` option, which defaults to `FALSE`. -- Exploited in usethis tests: option is set and unset in `setup.R` and `teardown.R`. Eliminates the need for ubiquitous `capture_output()` calls. -- Other packages can muffle a usethis call via, e.g., `withr::local_options(list(usethis.quiet = TRUE))`. -- Use `ui_silence()` for executing small bits of code silently. +- Exploited in usethis tests: option is set and unset in `setup.R` and `teardown.R`, to suppress output by default. +- Individual tests that need output, e.g. for snapshot testing, should include `withr::local_options(usethis.quiet = FALSE)`. +- Other packages can muffle a usethis call through similar means. +- `ui_silence()` is a convenience helper for executing small bits of code silently. + +Note that usethis exports many `ui_*()` functions for historical reasons, all defined in `ui-legacy.R`, and all marked as superseded now. +See the developer-facing article "Converting usethis's UI to use cli" for more. ## Git/GitHub diff --git a/tests/testthat/_snaps/author.md b/tests/testthat/_snaps/author.md index 188af7201..48bbed63c 100644 --- a/tests/testthat/_snaps/author.md +++ b/tests/testthat/_snaps/author.md @@ -3,16 +3,16 @@ Code challenge_legacy_author_fields() Message - x Found legacy Author and/or Maintainer field in DESCRIPTION. - usethis only supports modification of the Authors@R field. + x Found legacy 'Author' and/or 'Maintainer' field in DESCRIPTION. + usethis only supports modification of the 'Authors@R' field. i We recommend one of these paths forward: - * Delete these fields and rebuild with `use_author()`. - * Convert to Authors@R with `desc::desc_coerce_authors_at_r()`, - then delete the legacy fields. + [ ] Delete the legacy fields and rebuild with `use_author()`; or + [ ] Convert to 'Authors@R' with `desc::desc_coerce_authors_at_r()`, then delete + the legacy fields. Condition - Error: - ! User input required, but session is not interactive. - Query: Do you want to cancel this operation and sort that out first? + Error in `ui_yep()`: + x User input required, but session is not interactive. + i Query: "Do you want to cancel this operation and sort that out first?" # Decline to tweak an existing author @@ -20,16 +20,16 @@ use_author("Jennifer", "Bryan", role = "cph") Condition Error in `check_author_is_novel()`: - ! "Jennifer Bryan" already appears in "Authors@R". - Please make the desired change directly in DESCRIPTION or call the desc package directly. + x "Jennifer Bryan" already appears in 'Authors@R'. + Please make the desired change directly in DESCRIPTION or call the desc package directly. # Placeholder author is challenged Code use_author("Charlie", "Brown") Message - v Adding to Authors@R in DESCRIPTION: + v Adding to 'Authors@R' in DESCRIPTION: Charlie Brown [ctb] - i Authors@R appears to include a placeholder author: + i 'Authors@R' appears to include a placeholder author: First Last [aut, cre] (YOUR-ORCID-ID) diff --git a/tests/testthat/_snaps/badge.md b/tests/testthat/_snaps/badge.md index ec4f8af43..a212bab6b 100644 --- a/tests/testthat/_snaps/badge.md +++ b/tests/testthat/_snaps/badge.md @@ -29,7 +29,7 @@ use_posit_cloud_badge("http://posit.cloud/123") Condition Error in `use_posit_cloud_badge()`: - ! `usethis::use_posit_cloud_badge()` requires a link to an existing Posit Cloud project of the form "https://posit.cloud/content/" or "https://posit.cloud/spaces//content/". + x `usethis::use_posit_cloud_badge()` requires a link to an existing Posit Cloud project of the form "https://posit.cloud/content/" or "https://posit.cloud/spaces//content/". --- @@ -37,7 +37,7 @@ use_rscloud_badge("https://rstudio.cloud/content/123") Condition Error in `use_posit_cloud_badge()`: - ! `usethis::use_posit_cloud_badge()` requires a link to an existing Posit Cloud project of the form "https://posit.cloud/content/" or "https://posit.cloud/spaces//content/". + x `usethis::use_posit_cloud_badge()` requires a link to an existing Posit Cloud project of the form "https://posit.cloud/content/" or "https://posit.cloud/spaces//content/". --- @@ -45,5 +45,5 @@ use_rscloud_badge("https://posit.cloud/project/123") Condition Error in `use_posit_cloud_badge()`: - ! `usethis::use_posit_cloud_badge()` requires a link to an existing Posit Cloud project of the form "https://posit.cloud/content/" or "https://posit.cloud/spaces//content/". + x `usethis::use_posit_cloud_badge()` requires a link to an existing Posit Cloud project of the form "https://posit.cloud/content/" or "https://posit.cloud/spaces//content/". diff --git a/tests/testthat/_snaps/course.md b/tests/testthat/_snaps/course.md index eb684aa7e..34784853a 100644 --- a/tests/testthat/_snaps/course.md +++ b/tests/testthat/_snaps/course.md @@ -8,23 +8,23 @@ Code out <- download_url(url = "URL", destfile = "destfile") Message - i Retrying download ... attempt 2 + i Retrying download ... attempt 2. --- Code out <- download_url(url = "URL", destfile = "destfile") Message - i Retrying download ... attempt 2 - i Retrying download ... attempt 3 + i Retrying download ... attempt 2. + i Retrying download ... attempt 3. --- Code out <- download_url(url = "URL", destfile = "destfile", n_tries = 3) Message - i Retrying download ... attempt 2 - i Retrying download ... attempt 3 + i Retrying download ... attempt 2. + i Retrying download ... attempt 3. Condition Error: ! try 3 @@ -34,9 +34,9 @@ Code out <- download_url(url = "URL", destfile = "destfile", n_tries = 10) Message - i Retrying download ... attempt 2 - i Retrying download ... attempt 3 - i Retrying download ... attempt 4 + i Retrying download ... attempt 2. + i Retrying download ... attempt 3. + i Retrying download ... attempt 4. # normalize_url() prepends https:// (or not) diff --git a/tests/testthat/_snaps/cpp11.md b/tests/testthat/_snaps/cpp11.md index 44c084def..81bfeb151 100644 --- a/tests/testthat/_snaps/cpp11.md +++ b/tests/testthat/_snaps/cpp11.md @@ -3,5 +3,5 @@ Code check_cpp_register_deps() Message - * Now install 'decor', 'vctrs' to use cpp11. + [ ] Now install decor and vctrs to use cpp11. diff --git a/tests/testthat/_snaps/data-table.md b/tests/testthat/_snaps/data-table.md index f1aa70eaa..4123fc57c 100644 --- a/tests/testthat/_snaps/data-table.md +++ b/tests/testthat/_snaps/data-table.md @@ -15,6 +15,20 @@ # use_data_table() blocks use of Depends + Code + use_data_table() + Message + ! data.table should be in 'Imports' or 'Suggests', not 'Depends'! + v Removing data.table from 'Depends'. + v Adding data.table to 'Imports' field in DESCRIPTION. + v Adding "@importFrom data.table data.table", "@importFrom data.table :=", + "@importFrom data.table .SD", "@importFrom data.table .BY", "@importFrom + data.table .N", "@importFrom data.table .I", "@importFrom data.table .GRP", + "@importFrom data.table .NGRP", and "@importFrom data.table .EACHI" to + 'R/{TESTPKG}-package.R'. + +--- + Code roxygen_ns_show() Output diff --git a/tests/testthat/_snaps/git-default-branch.md b/tests/testthat/_snaps/git-default-branch.md index d3853d54d..9c24be952 100644 --- a/tests/testthat/_snaps/git-default-branch.md +++ b/tests/testthat/_snaps/git-default-branch.md @@ -3,14 +3,14 @@ Code git_default_branch_rename() Message - i Local branch 'master' appears to play the role of the default branch. - v Moving local 'master' branch to 'main'. - * Be sure to update files that refer to the default branch by name. - Consider searching within your project for 'master'. - x These GitHub Action files don't mention the new default branch 'main': - - '.github/workflows/blah.yml' - x Some badges may refer to the old default branch 'master': - - 'README.md' - x The bookdown configuration file may refer to the old default branch 'master': - - 'whatever/foo/_bookdown.yaml' + i Local branch "master" appears to play the role of the default branch. + v Moving local "master" branch to "main". + [ ] Be sure to update files that refer to the default branch by name. + Consider searching within your project for "master". + x This GitHub Action file doesn't mention the new default branch "main": + '.github/workflows/blah.yml' + x Some badges appear to refer to the old default branch "master". + [ ] Check and correct, if needed, in this file: 'README.md' + x The bookdown configuration file may refer to the old default branch "master". + [ ] Check and correct, if needed, in this file: 'whatever/foo/_bookdown.yaml' diff --git a/tests/testthat/_snaps/github-actions.md b/tests/testthat/_snaps/github-actions.md index 6f4083899..e11c0056e 100644 --- a/tests/testthat/_snaps/github-actions.md +++ b/tests/testthat/_snaps/github-actions.md @@ -4,13 +4,15 @@ use_github_action(url = "https://raw.githubusercontent.com/r-lib/actions/v2/examples/check-full.yaml", readme = "https://github.com/r-lib/actions/blob/v2/examples/README.md") Message - v Creating '.github/' - v Adding '^\\.github$' to '.Rbuildignore' - v Adding '*.html' to '.github/.gitignore' - v Creating '.github/workflows/' - v Saving 'r-lib/actions/examples/check-full.yaml@v2' to '.github/workflows/R-CMD-check.yaml' - * Learn more at . - v Adding R-CMD-check badge to 'README.md' + v Creating '.github/'. + v Adding "^\\.github$" to '.Rbuildignore'. + v Adding "*.html" to '.github/.gitignore'. + v Creating '.github/workflows/'. + v Saving "r-lib/actions/examples/check-full.yaml@v2" to + '.github/workflows/R-CMD-check.yaml'. + [ ] Learn more at + . + v Adding "R-CMD-check badge" to 'README.md'. # use_github_action() still errors in non-interactive environment @@ -32,7 +34,7 @@ Code check_uses_github_actions() Condition - Error: - ! Cannot detect that package '{TESTPKG}' already uses GitHub Actions. - Do you need to run `use_github_actions()`? + Error in `check_uses_github_actions()`: + x Cannot detect that package {TESTPKG} already uses GitHub Actions. + i Do you need to run `use_github_action()`? diff --git a/tests/testthat/_snaps/github.md b/tests/testthat/_snaps/github.md index 6a1160a13..d1be44e33 100644 --- a/tests/testthat/_snaps/github.md +++ b/tests/testthat/_snaps/github.md @@ -3,6 +3,6 @@ Code use_github_links(overwrite = FALSE) Condition - Error: - ! URL has a different value in DESCRIPTION. Use `overwrite = TRUE` to overwrite. + Error in `proj_desc_field_update()`: + x 'URL' has a different value in DESCRIPTION. Use `overwrite = TRUE` to overwrite. diff --git a/tests/testthat/_snaps/helpers.md b/tests/testthat/_snaps/helpers.md index dac7b271b..807c258c3 100644 --- a/tests/testthat/_snaps/helpers.md +++ b/tests/testthat/_snaps/helpers.md @@ -1,9 +1,66 @@ +# we message for new type and are silent for same type + + Code + use_dependency("crayon", "Imports") + Message + v Adding crayon to 'Imports' field in DESCRIPTION. + +# we message for version change and are silent for same version + + Code + use_dependency("crayon", "Imports") + Message + v Adding crayon to 'Imports' field in DESCRIPTION. + +--- + + Code + use_dependency("crayon", "Imports", min_version = "1.0.0") + Message + v Increasing crayon version to ">= 1.0.0" in DESCRIPTION. + +--- + + Code + use_dependency("crayon", "Imports", min_version = "2.0.0") + Message + v Increasing crayon version to ">= 2.0.0" in DESCRIPTION. + +# use_dependency() upgrades a dependency + + Code + use_dependency("usethis", "Suggests") + Message + v Adding usethis to 'Suggests' field in DESCRIPTION. + +--- + + Code + use_dependency("usethis", "Imports") + Message + v Moving usethis from 'Suggests' to 'Imports' field in DESCRIPTION. + +# use_dependency() declines to downgrade a dependency + + Code + use_dependency("usethis", "Imports") + Message + v Adding usethis to 'Imports' field in DESCRIPTION. + +--- + + Code + use_dependency("usethis", "Suggests") + Message + ! Package usethis is already listed in 'Imports' in DESCRIPTION; no change + made. + # can add LinkingTo dependency if other dependency already exists Code use_dependency("rlang", "LinkingTo") Message - v Adding 'rlang' to LinkingTo field in DESCRIPTION + v Adding rlang to 'LinkingTo' field in DESCRIPTION. # use_dependency() does not fall over on 2nd LinkingTo request @@ -15,6 +72,6 @@ Code use_package("rlang") Message - v Moving 'rlang' from Suggests to Imports field in DESCRIPTION - * Refer to functions with `rlang::fun()` + v Moving rlang from 'Suggests' to 'Imports' field in DESCRIPTION. + [ ] Refer to functions with `rlang::fun()`. diff --git a/tests/testthat/_snaps/lifecycle.md b/tests/testthat/_snaps/lifecycle.md index b19b30936..c284edbc9 100644 --- a/tests/testthat/_snaps/lifecycle.md +++ b/tests/testthat/_snaps/lifecycle.md @@ -3,13 +3,13 @@ Code use_lifecycle() Message - v Adding 'lifecycle' to Imports field in DESCRIPTION - * Refer to functions with `lifecycle::fun()` - v Adding '@importFrom lifecycle deprecated' to 'R/{TESTPKG}-package.R' - v Writing 'NAMESPACE' - v Creating 'man/figures/' - v Copied SVG badges to 'man/figures/' - * Add badges in documentation topics by inserting one of: + v Adding lifecycle to 'Imports' field in DESCRIPTION. + [ ] Refer to functions with `lifecycle::fun()`. + v Adding "@importFrom lifecycle deprecated" to 'R/{TESTPKG}-package.R'. + v Writing 'NAMESPACE'. + v Creating 'man/figures/'. + v Copied SVG badges to 'man/figures/'. + [ ] Add badges in documentation topics by inserting a line like this: #' `r lifecycle::badge('experimental')` #' `r lifecycle::badge('superseded')` #' `r lifecycle::badge('deprecated')` diff --git a/tests/testthat/_snaps/package.md b/tests/testthat/_snaps/package.md index dbf92e275..96501fa5e 100644 --- a/tests/testthat/_snaps/package.md +++ b/tests/testthat/_snaps/package.md @@ -3,60 +3,61 @@ Code use_package("withr") Message - v Adding 'withr' to Imports field in DESCRIPTION - * Refer to functions with `withr::fun()` + v Adding withr to 'Imports' field in DESCRIPTION. + [ ] Refer to functions with `withr::fun()`. Code use_package("withr") use_package("withr", "Suggests") - Condition - Warning: - Package 'withr' is already listed in 'Imports' in DESCRIPTION, no change made. + Message + ! Package withr is already listed in 'Imports' in DESCRIPTION; no change made. # use_package() handles R versions with aplomb Code use_package("R") Condition - Error: - ! Set `type = "Depends"` when specifying an R version + Error in `use_dependency()`: + x Set `type = "Depends"` when specifying an R version. --- Code use_package("R", type = "Depends") Condition - Error: - ! Specify `min_version` when `package = "R"` + Error in `use_dependency()`: + x Specify `min_version` when `package = "R"`. --- Code use_package("R", type = "Depends", min_version = "3.6") Message - v Adding 'R' to Depends field in DESCRIPTION + v Adding R to 'Depends' field in DESCRIPTION. --- Code use_package("R", type = "Depends", min_version = TRUE) Message - v Increasing 'R' version to '>= 4.1' in DESCRIPTION + v Increasing R version to ">= 4.1" in DESCRIPTION. # use_package(type = 'Suggests') guidance w/o and w/ rlang Code use_package("withr", "Suggests") Message - v Adding 'withr' to Suggests field in DESCRIPTION - * Use `requireNamespace("withr", quietly = TRUE)` to test if package is installed - * Then directly refer to functions with `withr::fun()` + v Adding withr to 'Suggests' field in DESCRIPTION. + [ ] Use `requireNamespace("withr", quietly = TRUE)` to test if package is + installed. + [ ] Then directly refer to functions with `withr::fun()`. --- Code use_package("purrr", "Suggests") Message - v Adding 'purrr' to Suggests field in DESCRIPTION - * In your package code, use `rlang::is_installed("purrr")` or `rlang::check_installed("purrr")` to test if purrr is installed - * Then directly refer to functions with `purrr::fun()` + v Adding purrr to 'Suggests' field in DESCRIPTION. + [ ] In your package code, use `rlang::is_installed("purrr")` or + `rlang::check_installed("purrr")` to test if purrr is installed. + [ ] Then directly refer to functions with `purrr::fun()`. diff --git a/tests/testthat/_snaps/pkgdown.md b/tests/testthat/_snaps/pkgdown.md index d1ff23874..75ac9ba7d 100644 --- a/tests/testthat/_snaps/pkgdown.md +++ b/tests/testthat/_snaps/pkgdown.md @@ -3,8 +3,28 @@ Code use_pkgdown() Message - v Adding '^_pkgdown\\.yml$', '^docs$', '^pkgdown$' to '.Rbuildignore' - v Adding 'docs' to '.gitignore' - v Writing '_pkgdown.yml' - * Edit '_pkgdown.yml' + v Adding "^_pkgdown\\.yml$", "^docs$", and "^pkgdown$" to '.Rbuildignore'. + v Adding "docs" to '.gitignore'. + v Writing '_pkgdown.yml'. + [ ] Edit '_pkgdown.yml'. + +# pkgdown_url() returns correct data, warns if pedantic + + Code + pkgdown_url(pedantic = TRUE) + Message + ! pkgdown config does not specify the site's 'url', which is optional but + recommended. + Output + NULL + +--- + + Code + pkgdown_url(pedantic = TRUE) + Message + ! pkgdown config does not specify the site's 'url', which is optional but + recommended. + Output + NULL diff --git a/tests/testthat/_snaps/proj-desc.md b/tests/testthat/_snaps/proj-desc.md index 9c787aa0f..8d75d6819 100644 --- a/tests/testthat/_snaps/proj-desc.md +++ b/tests/testthat/_snaps/proj-desc.md @@ -3,17 +3,17 @@ Code proj_desc_field_update("Config/Needs/foofy", "alfa", append = TRUE) Message - v Adding 'alfa' to Config/Needs/foofy + v Adding "alfa" to 'Config/Needs/foofy'. Code proj_desc_field_update("Config/Needs/foofy", "alfa", append = TRUE) proj_desc_field_update("Config/Needs/foofy", "bravo", append = TRUE) Message - v Adding 'bravo' to Config/Needs/foofy + v Adding "bravo" to 'Config/Needs/foofy'. # proj_desc_field_update() works with multiple values Code proj_desc_field_update("Config/Needs/foofy", c("alfa", "bravo"), append = TRUE) Message - v Adding 'alfa', 'bravo' to Config/Needs/foofy + v Adding "alfa" and "bravo" to 'Config/Needs/foofy'. diff --git a/tests/testthat/_snaps/proj.md b/tests/testthat/_snaps/proj.md index 97308bbeb..a54abb4d3 100644 --- a/tests/testthat/_snaps/proj.md +++ b/tests/testthat/_snaps/proj.md @@ -3,22 +3,22 @@ Code proj_path(c("/a", "b", "/c")) Condition - Error: - ! Paths must be relative to the active project + Error in `proj_path()`: + x Paths must be relative to the active project, not absolute. --- Code proj_path("/a", "b", "/c") Condition - Error: - ! Paths must be relative to the active project + Error in `proj_path()`: + x Paths must be relative to the active project, not absolute. --- Code proj_path("/a", c("b", "/c")) Condition - Error: - ! Paths must be relative to the active project + Error in `proj_path()`: + x Paths must be relative to the active project, not absolute. diff --git a/tests/testthat/_snaps/rename-files.md b/tests/testthat/_snaps/rename-files.md index 731216fd6..a110ec3f3 100644 --- a/tests/testthat/_snaps/rename-files.md +++ b/tests/testthat/_snaps/rename-files.md @@ -3,5 +3,5 @@ Code rename_files("foo", "bar") Message - v Moving 'src/foo.c', 'src/foo.h' to 'src/bar.c', 'src/bar.h' + v Moving 'src/foo.c' and 'src/foo.h' to 'src/bar.c' and 'src/bar.h'. diff --git a/tests/testthat/_snaps/roxygen.md b/tests/testthat/_snaps/roxygen.md new file mode 100644 index 000000000..548c6a0fb --- /dev/null +++ b/tests/testthat/_snaps/roxygen.md @@ -0,0 +1,16 @@ +# use_package_doc() compatible with roxygen_ns_append() + + Code + use_package_doc() + Message + v Writing 'R/{TESTPKG}-package.R'. + +--- + + Code + roxygen_ns_append("test") + Message + v Adding "test" to 'R/{TESTPKG}-package.R'. + Output + [1] TRUE + diff --git a/tests/testthat/_snaps/tibble.md b/tests/testthat/_snaps/tibble.md index ac0695750..de3efaf45 100644 --- a/tests/testthat/_snaps/tibble.md +++ b/tests/testthat/_snaps/tibble.md @@ -3,8 +3,8 @@ Code use_tibble() Message - v Adding 'tibble' to Imports field in DESCRIPTION - v Adding '@importFrom tibble tibble' to 'R/{TESTPKG}-package.R' - * Document a returned tibble like so: + v Adding tibble to 'Imports' field in DESCRIPTION. + v Adding "@importFrom tibble tibble" to 'R/{TESTPKG}-package.R'. + [ ] Document a returned tibble like so: #' @return a [tibble][tibble::tibble-package] diff --git a/tests/testthat/_snaps/tidyverse.md b/tests/testthat/_snaps/tidyverse.md index b70cd5aa3..dcb9680d6 100644 --- a/tests/testthat/_snaps/tidyverse.md +++ b/tests/testthat/_snaps/tidyverse.md @@ -3,14 +3,15 @@ Code use_tidy_dependencies() Message - v Adding 'rlang' to Imports field in DESCRIPTION - v Adding 'lifecycle' to Imports field in DESCRIPTION - v Adding 'cli' to Imports field in DESCRIPTION - v Adding 'glue' to Imports field in DESCRIPTION - v Adding 'withr' to Imports field in DESCRIPTION - v Adding '@import rlang' to 'R/{TESTPKG}-package.R' - v Adding '@importFrom glue glue' to 'R/{TESTPKG}-package.R' - v Adding '@importFrom lifecycle deprecated' to 'R/{TESTPKG}-package.R' - v Writing 'NAMESPACE' - v Writing 'R/import-standalone-purrr.R' + v Adding rlang to 'Imports' field in DESCRIPTION. + v Adding lifecycle to 'Imports' field in DESCRIPTION. + v Adding cli to 'Imports' field in DESCRIPTION. + v Adding glue to 'Imports' field in DESCRIPTION. + v Adding withr to 'Imports' field in DESCRIPTION. + v Adding "@import rlang" to 'R/{TESTPKG}-package.R'. + v Adding "@importFrom glue glue" to 'R/{TESTPKG}-package.R'. + v Adding "@importFrom lifecycle deprecated" to + 'R/{TESTPKG}-package.R'. + v Writing 'NAMESPACE'. + v Writing 'R/import-standalone-purrr.R'. diff --git a/tests/testthat/_snaps/ui.md b/tests/testthat/_snaps/ui-legacy.md similarity index 65% rename from tests/testthat/_snaps/ui.md rename to tests/testthat/_snaps/ui-legacy.md index dfb8fea2e..7dbebf21a 100644 --- a/tests/testthat/_snaps/ui.md +++ b/tests/testthat/_snaps/ui-legacy.md @@ -1,4 +1,4 @@ -# basic UI actions behave as expected +# basic legacy UI actions behave as expected Code ui_line("line") @@ -31,13 +31,3 @@ Warning: a warning -# UI actions respect usethis.quiet = TRUE - - Code - ui_line("line") - ui_todo("to do") - ui_done("done") - ui_oops("oops") - ui_info("info") - ui_code_block(c("x <- 1", "y <- 2")) - diff --git a/tests/testthat/_snaps/use_import_from.md b/tests/testthat/_snaps/use_import_from.md index 669911af6..857fe0ef3 100644 --- a/tests/testthat/_snaps/use_import_from.md +++ b/tests/testthat/_snaps/use_import_from.md @@ -11,18 +11,18 @@ Code use_import_from(1) Condition - Error: - ! `package` must be a single string + Error in `use_import_from()`: + x `package` must be a single string. Code use_import_from(c("tibble", "rlang")) Condition - Error: - ! `package` must be a single string + Error in `use_import_from()`: + x `package` must be a single string. Code use_import_from("tibble", "pool_noodle") Condition Error in `map2()`: i In index: 1. - Caused by error: - ! Can't find `tibble::pool_noodle()` + Caused by error in `.f()`: + x Can't find `tibble::pool_noodle()`. diff --git a/tests/testthat/_snaps/utils-github.md b/tests/testthat/_snaps/utils-github.md index 64a22bc7c..d8d1feb03 100644 --- a/tests/testthat/_snaps/utils-github.md +++ b/tests/testthat/_snaps/utils-github.md @@ -9,18 +9,188 @@ [7] "fork_cannot_push_origin" "fork_upstream_is_not_origin_parent" [9] "upstream_but_origin_is_not_fork" -# fork_upstream_is_not_origin_parent is detected +# 'no_github' is reported correctly + + Code + new_no_github() + Message + * Type = "no_github" + * Host = "NA" + * Config supports a pull request = FALSE + * origin = + * upstream = + ! Neither "origin" nor "upstream" is a GitHub repo. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# 'ours' is reported correctly + + Code + new_ours() + Message + * Type = "ours" + * Host = "https://github.com" + * Config supports a pull request = TRUE + * origin = "OWNER/REPO" (can push) + * upstream = + i "origin" is both the source and primary repo. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# 'theirs' is reported correctly + + Code + new_theirs() + Message + * Type = "theirs" + * Host = "https://github.com" + * Config supports a pull request = FALSE + * origin = "OWNER/REPO" (can not push) + * upstream = + ! The only configured GitHub remote is "origin", which you cannot push to. + i If your goal is to make a pull request, you must fork-and-clone. + i `usethis::create_from_github()` can do this. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# 'fork' is reported correctly + + Code + new_fork() + Message + * Type = "fork" + * Host = "https://github.com" + * Config supports a pull request = TRUE + * origin = "CONTRIBUTOR/REPO" (can push) = fork of "OWNER/REPO" + * upstream = "OWNER/REPO" (can not push) + i "origin" is a fork of "OWNER/REPO", which is configured as the "upstream" + remote. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# 'maybe_ours_or_theirs' is reported correctly + + Code + new_maybe_ours_or_theirs() + Message + * Type = "maybe_ours_or_theirs" + * Host = "https://github.com" + * Config supports a pull request = NA + * origin = "OWNER/REPO" + * upstream = + ! "origin" is a GitHub repo and "upstream" is either not configured or is not a + GitHub repo. + i We may be offline or you may need to configure a GitHub personal access + token. + i `usethis::gh_token_help()` can help with that. + i Read more about what this GitHub remote configuration means at: + . + +# 'maybe_fork' is reported correctly + + Code + new_maybe_fork() + Message + * Type = "maybe_fork" + * Host = "https://github.com" + * Config supports a pull request = NA + * origin = "CONTRIBUTOR/REPO" + * upstream = "OWNER/REPO" + ! Both "origin" and "upstream" appear to be GitHub repos. However, we can't + confirm their relationship to each other (e.g., fork and fork parent) or your + permissions (e.g. push access). + i We may be offline or you may need to configure a GitHub personal access + token. + i `usethis::gh_token_help()` can help with that. + i Read more about what this GitHub remote configuration means at: + . + +# 'fork_cannot_push_origin' is reported correctly + + Code + new_fork_cannot_push_origin() + Message + * Type = "fork_cannot_push_origin" + * Host = "https://github.com" + * Config supports a pull request = FALSE + * origin = "CONTRIBUTOR/REPO" + * upstream = "OWNER/REPO" + ! The "origin" remote is a fork, but you can't push to it. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# 'fork_upstream_is_not_origin_parent' is reported correctly + + Code + new_fork_upstream_is_not_origin_parent() + Message + * Type = "fork_upstream_is_not_origin_parent" + * Host = "https://github.com" + * Config supports a pull request = FALSE + * origin = "CONTRIBUTOR/REPO" (can push) = fork of "NEW_OWNER/REPO" + * upstream = "OLD_OWNER/REPO" (can not push) + ! The "origin" GitHub remote is a fork, but its parent is not configured as the + "upstream" remote. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# 'upstream_but_origin_is_not_fork' is reported correctly + + Code + new_upstream_but_origin_is_not_fork() + Message + * Type = "upstream_but_origin_is_not_fork" + * Host = "https://github.com" + * Config supports a pull request = FALSE + * origin = "CONTRIBUTOR/REPO" + * upstream = "OWNER/REPO" + ! Both "origin" and "upstream" are GitHub remotes, but "origin" is not a fork + and, in particular, is not a fork of "upstream". + i Read more about the GitHub remote configurations that usethis supports at: + . + +# 'fork_upstream_is_not_origin_parent' is detected correctly Code stop_bad_github_remote_config(cfg) Condition Error in `stop_bad_github_remote_config()`: - ! Unsupported GitHub remote configuration: 'fork_upstream_is_not_origin_parent' - * Host = 'https://github.com' - * origin = 'jennybc/gh' (can push) = fork of NA - * upstream = 'r-pkgs/gh' (can push) - * The 'origin' GitHub remote is a fork, but its parent is not configured as the 'upstream' remote. - - Read more about the GitHub remote configurations that usethis supports at: - 'https://happygitwithr.com/common-remote-setups.html' + x Unsupported GitHub remote configuration: "fork_upstream_is_not_origin_parent" + * Host = "https://github.com" + * origin = "jennybc/gh" (can push) = fork of "r-lib/gh" + * upstream = "r-pkgs/gh" (can push) + ! The "origin" GitHub remote is a fork, but its parent is not configured as the "upstream" remote. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# bad github config error + + Code + stop_bad_github_remote_config(new_fork_upstream_is_not_origin_parent()) + Condition + Error in `stop_bad_github_remote_config()`: + x Unsupported GitHub remote configuration: "fork_upstream_is_not_origin_parent" + * Host = "https://github.com" + * origin = "CONTRIBUTOR/REPO" (can push) = fork of "NEW_OWNER/REPO" + * upstream = "OLD_OWNER/REPO" (can not push) + ! The "origin" GitHub remote is a fork, but its parent is not configured as the "upstream" remote. + i Read more about the GitHub remote configurations that usethis supports at: + . + +# maybe bad github config error + + Code + stop_maybe_github_remote_config(new_maybe_fork()) + Condition + Error in `stop_maybe_github_remote_config()`: + x Pull request functions can't work with GitHub remote configuration: "maybe_fork". + i The most likely problem is that we aren't discovering your GitHub personal access token. + * Host = "https://github.com" + * origin = "CONTRIBUTOR/REPO" + * upstream = "OWNER/REPO" + ! Both "origin" and "upstream" appear to be GitHub repos. However, we can't confirm their relationship to each other (e.g., fork and fork parent) or your permissions (e.g. push access). + i We may be offline or you may need to configure a GitHub personal access token. + i `usethis::gh_token_help()` can help with that. + i Read more about what this GitHub remote configuration means at: + . diff --git a/tests/testthat/_snaps/utils-ui.md b/tests/testthat/_snaps/utils-ui.md new file mode 100644 index 000000000..b738a1af3 --- /dev/null +++ b/tests/testthat/_snaps/utils-ui.md @@ -0,0 +1,455 @@ +# ui_bullets() look as expected [plain] + + Code + ui_bullets(c(`_` = "todo", v = "done", x = "oops", i = "info", "noindent", ` ` = "indent", + `*` = "bullet", `>` = "arrow", `!` = "warning")) + Message + [ ] todo + v done + x oops + i info + noindent + indent + * bullet + > arrow + ! warning + +# ui_bullets() look as expected [ansi] + + Code + ui_bullets(c(`_` = "todo", v = "done", x = "oops", i = "info", "noindent", ` ` = "indent", + `*` = "bullet", `>` = "arrow", `!` = "warning")) + Message + [ ] todo + v done + x oops + i info + noindent + indent + * bullet + > arrow + ! warning + +# ui_bullets() look as expected [unicode] + + Code + ui_bullets(c(`_` = "todo", v = "done", x = "oops", i = "info", "noindent", ` ` = "indent", + `*` = "bullet", `>` = "arrow", `!` = "warning")) + Message + ☐ todo + ✔ done + ✖ oops + ℹ info + noindent + indent + • bullet + → arrow + ! warning + +# ui_bullets() look as expected [fancy] + + Code + ui_bullets(c(`_` = "todo", v = "done", x = "oops", i = "info", "noindent", ` ` = "indent", + `*` = "bullet", `>` = "arrow", `!` = "warning")) + Message + ☐ todo + ✔ done + ✖ oops + ℹ info + noindent + indent + • bullet + → arrow + ! warning + +# ui_bullets() does glue interpolation and inline markup [plain] + + Code + ui_bullets(c(i = "Hello, {x}!", v = "Updated the {.field BugReports} field", x = "Scary {.code code} or {.fun function}")) + Message + i Hello, world! + v Updated the 'BugReports' field + x Scary `code` or `function()` + +# ui_bullets() does glue interpolation and inline markup [ansi] + + Code + ui_bullets(c(i = "Hello, {x}!", v = "Updated the {.field BugReports} field", x = "Scary {.code code} or {.fun function}")) + Message + i Hello, world! + v Updated the BugReports field + x Scary `code` or `function()` + +# ui_bullets() does glue interpolation and inline markup [unicode] + + Code + ui_bullets(c(i = "Hello, {x}!", v = "Updated the {.field BugReports} field", x = "Scary {.code code} or {.fun function}")) + Message + ℹ Hello, world! + ✔ Updated the 'BugReports' field + ✖ Scary `code` or `function()` + +# ui_bullets() does glue interpolation and inline markup [fancy] + + Code + ui_bullets(c(i = "Hello, {x}!", v = "Updated the {.field BugReports} field", x = "Scary {.code code} or {.fun function}")) + Message + ℹ Hello, world! + ✔ Updated the BugReports field + ✖ Scary `code` or `function()` + +# ui_abort() defaults to 'x' for first bullet + + Code + ui_abort("no explicit bullet") + Condition + Error: + x no explicit bullet + +# ui_abort() can take explicit first bullet + + Code + ui_abort(c(v = "success bullet")) + Condition + Error: + v success bullet + +# ui_abort() defaults to 'i' for non-first bullet + + Code + ui_abort(c("oops", ` ` = "space bullet", "info bullet", v = "success bullet")) + Condition + Error: + x oops + space bullet + i info bullet + v success bullet + +# ui_code_snippet() with scalar input [plain] + + Code + ui_code_snippet( + "\n options(\n warnPartialMatchArgs = TRUE,\n warnPartialMatchDollar = TRUE,\n warnPartialMatchAttr = TRUE\n )") + Message + options( + warnPartialMatchArgs = TRUE, + warnPartialMatchDollar = TRUE, + warnPartialMatchAttr = TRUE + ) + +# ui_code_snippet() with scalar input [ansi] + + Code + ui_code_snippet( + "\n options(\n warnPartialMatchArgs = TRUE,\n warnPartialMatchDollar = TRUE,\n warnPartialMatchAttr = TRUE\n )") + Message + options( + warnPartialMatchArgs = TRUE, + warnPartialMatchDollar = TRUE, + warnPartialMatchAttr = TRUE + ) + +# ui_code_snippet() with vector input [plain] + + Code + ui_code_snippet(c("options(", " warnPartialMatchArgs = TRUE,", + " warnPartialMatchDollar = TRUE,", " warnPartialMatchAttr = TRUE", ")")) + Message + options( + warnPartialMatchArgs = TRUE, + warnPartialMatchDollar = TRUE, + warnPartialMatchAttr = TRUE + ) + +# ui_code_snippet() with vector input [ansi] + + Code + ui_code_snippet(c("options(", " warnPartialMatchArgs = TRUE,", + " warnPartialMatchDollar = TRUE,", " warnPartialMatchAttr = TRUE", ")")) + Message + options( + warnPartialMatchArgs = TRUE, + warnPartialMatchDollar = TRUE, + warnPartialMatchAttr = TRUE + ) + +# ui_code_snippet() when language is not R [plain] + + Code + ui_code_snippet("#include <{h}>", language = "") + Message + #include + +# ui_code_snippet() when language is not R [ansi] + + Code + ui_code_snippet("#include <{h}>", language = "") + Message + #include + +# ui_code_snippet() can interpolate [plain] + + Code + ui_code_snippet("if (1) {true_val} else {false_val}") + Message + if (1) TRUE else 'FALSE' + +# ui_code_snippet() can interpolate [ansi] + + Code + ui_code_snippet("if (1) {true_val} else {false_val}") + Message + if (1) TRUE else 'FALSE' + +# ui_code_snippet() can NOT interpolate [plain] + + Code + ui_code_snippet("foo <- function(x){x}", interpolate = FALSE) + Message + foo <- function(x){x} + Code + ui_code_snippet("foo <- function(x){{x}}", interpolate = TRUE) + Message + foo <- function(x){x} + +# ui_code_snippet() can NOT interpolate [ansi] + + Code + ui_code_snippet("foo <- function(x){x}", interpolate = FALSE) + Message + foo <- function(x){x} + Code + ui_code_snippet("foo <- function(x){{x}}", interpolate = TRUE) + Message + foo <- function(x){x} + +# bulletize() works + + Code + ui_bullets(bulletize(letters)) + Message + * a + * b + * c + * d + * e + ... and 21 more + +--- + + Code + ui_bullets(bulletize(letters, bullet = "x")) + Message + x a + x b + x c + x d + x e + ... and 21 more + +--- + + Code + ui_bullets(bulletize(letters, n_show = 2)) + Message + * a + * b + ... and 24 more + +--- + + Code + ui_bullets(bulletize(letters[1:6])) + Message + * a + * b + * c + * d + * e + * f + +--- + + Code + ui_bullets(bulletize(letters[1:7])) + Message + * a + * b + * c + * d + * e + * f + * g + +--- + + Code + ui_bullets(bulletize(letters[1:8])) + Message + * a + * b + * c + * d + * e + ... and 3 more + +--- + + Code + ui_bullets(bulletize(letters[1:6], n_fudge = 0)) + Message + * a + * b + * c + * d + * e + ... and 1 more + +--- + + Code + ui_bullets(bulletize(letters[1:8], n_fudge = 3)) + Message + * a + * b + * c + * d + * e + * f + * g + * h + +# ui_special() works [plain] + + Code + cli::cli_text(ui_special()) + Message + + +--- + + Code + cli::cli_text(ui_special("whatever")) + Message + + +# ui_special() works [ansi] + + Code + cli::cli_text(ui_special()) + Message +  + +--- + + Code + cli::cli_text(ui_special("whatever")) + Message +  + +# kv_line() looks as expected in basic use [plain] + + Code + kv_line("CHARACTER", "VALUE") + Message + * CHARACTER: "VALUE" + Code + kv_line("NUMBER", 1) + Message + * NUMBER: 1 + Code + kv_line("LOGICAL", TRUE) + Message + * LOGICAL: TRUE + +# kv_line() looks as expected in basic use [fancy] + + Code + kv_line("CHARACTER", "VALUE") + Message + • CHARACTER: "VALUE" + Code + kv_line("NUMBER", 1) + Message + • NUMBER: 1 + Code + kv_line("LOGICAL", TRUE) + Message + • LOGICAL: TRUE + +# kv_line() can interpolate and style inline in key [plain] + + Code + kv_line("Let's reveal {.field {field}}", "whatever") + Message + * Let's reveal 'SOME_FIELD': "whatever" + +# kv_line() can interpolate and style inline in key [fancy] + + Code + kv_line("Let's reveal {.field {field}}", "whatever") + Message + • Let's reveal SOME_FIELD: "whatever" + +# kv_line() can treat value in different ways [plain] + + Code + kv_line("Key", value) + Message + * Key: "some value" + Code + kv_line("Something we don't have", NULL) + Message + * Something we don't have: + Code + kv_line("Key", ui_special("discovered")) + Message + * Key: + Code + kv_line("Key", "something {.emph important}") + Message + * Key: "something {.emph important}" + Code + kv_line("Key", I("something {.emph important}")) + Message + * Key: something important + Code + kv_line("Key", I("something {.emph {adjective}}")) + Message + * Key: something great + Code + kv_line("Interesting file", I("{.url {url}}")) + Message + * Interesting file: + +# kv_line() can treat value in different ways [fancy] + + Code + kv_line("Key", value) + Message + • Key: "some value" + Code + kv_line("Something we don't have", NULL) + Message + • Something we don't have:  + Code + kv_line("Key", ui_special("discovered")) + Message + • Key:  + Code + kv_line("Key", "something {.emph important}") + Message + • Key: "something {.emph important}" + Code + kv_line("Key", I("something {.emph important}")) + Message + • Key: something important + Code + kv_line("Key", I("something {.emph {adjective}}")) + Message + • Key: something great + Code + kv_line("Interesting file", I("{.url {url}}")) + Message + • Interesting file:  + diff --git a/tests/testthat/_snaps/utils.md b/tests/testthat/_snaps/utils.md new file mode 100644 index 000000000..dd26dff81 --- /dev/null +++ b/tests/testthat/_snaps/utils.md @@ -0,0 +1,24 @@ +# check_is_named_list() works + + Code + user_facing_function(NULL) + Condition + Error in `check_is_named_list()`: + x `somevar` must be a list, not NULL. + +--- + + Code + user_facing_function(c(a = "a", b = "b")) + Condition + Error in `check_is_named_list()`: + x `somevar` must be a list, not a character vector. + +--- + + Code + user_facing_function(list("a", b = 2)) + Condition + Error in `check_is_named_list()`: + x Names of `somevar` must be non-missing, non-empty, and non-duplicated. + diff --git a/tests/testthat/_snaps/vignette.md b/tests/testthat/_snaps/vignette.md index de81c8c18..e7359ae1b 100644 --- a/tests/testthat/_snaps/vignette.md +++ b/tests/testthat/_snaps/vignette.md @@ -8,8 +8,8 @@ Code use_vignette("bad name") Condition - Error: - ! 'bad name' is not a valid filename for a vignette. It must: - * Start with a letter. - * Contain only letters, numbers, '_', and '-'. + Error in `check_vignette_name()`: + x "bad name" is not a valid filename for a vignette. It must: + i Start with a letter. + i Contain only letters, numbers, '_', and '-'. diff --git a/tests/testthat/_snaps/write.md b/tests/testthat/_snaps/write.md index 2b3e8a910..006f0e831 100644 --- a/tests/testthat/_snaps/write.md +++ b/tests/testthat/_snaps/write.md @@ -3,5 +3,5 @@ Code write_union(proj_path("somefile"), letters[4:6]) Message - v Adding 'd', 'e', 'f' to 'somefile' + v Adding "d", "e", and "f" to 'somefile'. diff --git a/tests/testthat/helper-mocks.R b/tests/testthat/helper-mocks.R index 845216a05..d2dc19ef4 100644 --- a/tests/testthat/helper-mocks.R +++ b/tests/testthat/helper-mocks.R @@ -22,8 +22,8 @@ local_check_fun_exists <- function(.env = caller_env()) { local_mocked_bindings(check_fun_exists = function(...) NULL, .env = .env) } -local_ui_yeah <- function(.env = caller_env()) { - local_mocked_bindings(ui_yeah = function(...) TRUE, .env = .env) +local_ui_yep <- function(.env = caller_env()) { + local_mocked_bindings(ui_yep = function(...) TRUE, .env = .env) } local_git_default_branch_remote <- function(.env = caller_env()) { diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 0a21f8555..ff3f76c6b 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -6,10 +6,10 @@ session_temp_proj <- proj_find(path_temp()) if (!is.null(session_temp_proj)) { Rproj_files <- fs::dir_ls(session_temp_proj, glob = "*.Rproj") - ui_line(c( - "Rproj file(s) found at or above session temp dir:", - paste0("* ", Rproj_files), - "Expect this to cause spurious test failures." + ui_bullets(c( + "x" = "Rproj {cli::qty(length(Rproj_files))} file{?s} found at or above session temp dir:", + bulletize(usethis_map_cli(Rproj_files)), + "!" = "Expect this to cause spurious test failures." )) } @@ -31,7 +31,7 @@ create_local_thing <- function(dir = file_temp(pattern = pattern), thing = c("package", "project")) { thing <- match.arg(thing) if (fs::dir_exists(dir)) { - ui_stop("Target {ui_code('dir')} {ui_path(dir)} already exists.") + ui_abort("Target {.arg dir} {.path {pth(dir)}} already exists.") } old_project <- proj_get_() # this could be `NULL`, i.e. no active project @@ -39,7 +39,7 @@ create_local_thing <- function(dir = file_temp(pattern = pattern), withr::defer( { - ui_done("Deleting temporary project: {ui_path(dir)}") + ui_bullets(c("Deleting temporary project: {.path {dir}}")) fs::dir_delete(dir) }, envir = env @@ -68,7 +68,7 @@ create_local_thing <- function(dir = file_temp(pattern = pattern), withr::defer( { - ui_done("Restoring original working directory: {ui_path(old_wd)}") + ui_bullets(c("Restoring original working directory: {.path {old_wd}}")) setwd(old_wd) }, envir = env diff --git a/tests/testthat/test-data-table.R b/tests/testthat/test-data-table.R index 88dbdfd29..0a55f8482 100644 --- a/tests/testthat/test-data-table.R +++ b/tests/testthat/test-data-table.R @@ -26,11 +26,12 @@ test_that("use_data_table() blocks use of Depends", { local_roxygen_update_ns() local_check_fun_exists() - expect_warning( + withr::local_options(list(usethis.quiet = FALSE)) + expect_snapshot( use_data_table(), - "data.table should be in Imports or Suggests, not Depends" + transform = scrub_testpkg ) - expect_match(proj_desc()$get("Imports"), "data.table") + expect_match(desc::desc_get("Imports"), "data.table") expect_snapshot(roxygen_ns_show()) }) diff --git a/tests/testthat/test-helpers.R b/tests/testthat/test-helpers.R index 22ceb5cf1..3b063325d 100644 --- a/tests/testthat/test-helpers.R +++ b/tests/testthat/test-helpers.R @@ -30,31 +30,27 @@ test_that("valid_file_name() enforces valid file names", { test_that("we message for new type and are silent for same type", { create_local_package() - withr::local_options(list(usethis.quiet = FALSE, crayon.enabled = FALSE)) + withr::local_options(usethis.quiet = FALSE) - expect_message( - use_dependency("crayon", "Imports"), - "Adding 'crayon' to Imports field" + expect_snapshot( + use_dependency("crayon", "Imports") ) expect_silent(use_dependency("crayon", "Imports")) }) test_that("we message for version change and are silent for same version", { create_local_package() - withr::local_options(list(usethis.quiet = FALSE, crayon.enabled = FALSE)) + withr::local_options(usethis.quiet = FALSE) - expect_message( - use_dependency("crayon", "Imports"), - "Adding 'crayon" + expect_snapshot( + use_dependency("crayon", "Imports") ) - expect_message( - use_dependency("crayon", "Imports", min_version = "1.0.0"), - "Increasing 'crayon'" + expect_snapshot( + use_dependency("crayon", "Imports", min_version = "1.0.0") ) expect_silent(use_dependency("crayon", "Imports", min_version = "1.0.0")) - expect_message( - use_dependency("crayon", "Imports", min_version = "2.0.0"), - "Increasing 'crayon'" + expect_snapshot( + use_dependency("crayon", "Imports", min_version = "2.0.0") ) expect_silent(use_dependency("crayon", "Imports", min_version = "1.0.0")) }) @@ -62,12 +58,12 @@ test_that("we message for version change and are silent for same version", { ## https://github.com/r-lib/usethis/issues/99 test_that("use_dependency() upgrades a dependency", { create_local_package() - withr::local_options(list(usethis.quiet = FALSE, crayon.enabled = FALSE)) + withr::local_options(usethis.quiet = FALSE) - expect_message(use_dependency("usethis", "Suggests")) + expect_snapshot(use_dependency("usethis", "Suggests")) expect_match(desc::desc_get("Suggests"), "usethis") - expect_message(use_dependency("usethis", "Imports"), "Moving 'usethis'") + expect_snapshot(use_dependency("usethis", "Imports")) expect_match(desc::desc_get("Imports"), "usethis") expect_no_match(desc::desc_get("Suggests"), "usethis") }) @@ -75,21 +71,21 @@ test_that("use_dependency() upgrades a dependency", { ## https://github.com/r-lib/usethis/issues/99 test_that("use_dependency() declines to downgrade a dependency", { create_local_package() - withr::local_options(list(usethis.quiet = FALSE, crayon.enabled = FALSE)) + withr::local_options(usethis.quiet = FALSE) - expect_message(use_dependency("usethis", "Imports")) + expect_snapshot(use_dependency("usethis", "Imports")) expect_match(desc::desc_get("Imports"), "usethis") - expect_warning(use_dependency("usethis", "Suggests"), "no change") + expect_snapshot(use_dependency("usethis", "Suggests")) expect_match(desc::desc_get("Imports"), "usethis") - expect_no_match( desc::desc_get("Suggests"), "usethis") + expect_no_match(desc::desc_get("Suggests"), "usethis") }) test_that("can add LinkingTo dependency if other dependency already exists", { create_local_package() use_dependency("rlang", "Imports") - withr::local_options(list(usethis.quiet = FALSE)) + withr::local_options(usethis.quiet = FALSE) expect_snapshot( use_dependency("rlang", "LinkingTo") ) @@ -104,7 +100,7 @@ test_that("use_dependency() does not fall over on 2nd LinkingTo request", { use_dependency("rlang", "LinkingTo") - withr::local_options(list(usethis.quiet = FALSE)) + withr::local_options(usethis.quiet = FALSE) expect_snapshot(use_dependency("rlang", "LinkingTo")) }) @@ -116,7 +112,7 @@ test_that("use_dependency() can level up a LinkingTo dependency", { use_dependency("rlang", "LinkingTo") use_dependency("rlang", "Suggests") - withr::local_options(list(usethis.quiet = FALSE)) + withr::local_options(usethis.quiet = FALSE) expect_snapshot(use_package("rlang")) deps <- proj_deps() diff --git a/tests/testthat/test-lifecycle.R b/tests/testthat/test-lifecycle.R index ae7a6ec5d..214bd0a1b 100644 --- a/tests/testthat/test-lifecycle.R +++ b/tests/testthat/test-lifecycle.R @@ -1,7 +1,7 @@ test_that("use_lifecycle() imports badges", { create_local_package() use_package_doc() - withr::local_options(usethis.quiet = FALSE) + withr::local_options(usethis.quiet = FALSE, cli.width = Inf) expect_snapshot( use_lifecycle(), diff --git a/tests/testthat/test-package.R b/tests/testthat/test-package.R index a58b05fd3..db4b44e52 100644 --- a/tests/testthat/test-package.R +++ b/tests/testthat/test-package.R @@ -40,7 +40,7 @@ test_that("use_package(type = 'Suggests') guidance w/o and w/ rlang", { test_that("use_dev_package() writes a remote", { create_local_package() - local_ui_yeah() + local_ui_yep() use_dev_package("usethis") expect_equal(proj_desc()$get_remotes(), "r-lib/usethis") @@ -59,7 +59,7 @@ test_that("package_remote() works for an installed package with github URL", { "Package: test", "URL: https://github.com/OWNER/test" )) - local_ui_yeah() + local_ui_yep() expect_equal(package_remote(d), "OWNER/test") }) diff --git a/tests/testthat/test-pkgdown.R b/tests/testthat/test-pkgdown.R index 7344c0eea..9ea3f5932 100644 --- a/tests/testthat/test-pkgdown.R +++ b/tests/testthat/test-pkgdown.R @@ -55,13 +55,18 @@ test_that("pkgdown_url() returns correct data, warns if pedantic", { # empty config expect_null(pkgdown_url()) expect_silent(pkgdown_url()) - expect_warning(pkgdown_url(pedantic = TRUE), "url") + withr::local_options(list(usethis.quiet = FALSE)) + expect_snapshot( + pkgdown_url(pedantic = TRUE) + ) # nonempty config, but no url writeLines(c("home:", " strip_header: true"), pkgdown_config_path()) expect_null(pkgdown_url()) expect_silent(pkgdown_url()) - expect_warning(pkgdown_url(pedantic = TRUE), "url") + expect_snapshot( + pkgdown_url(pedantic = TRUE) + ) # config has url writeLines("url: https://usethis.r-lib.org", pkgdown_config_path()) diff --git a/tests/testthat/test-roxygen.R b/tests/testthat/test-roxygen.R index 89ffc86ea..b3161bfa9 100644 --- a/tests/testthat/test-roxygen.R +++ b/tests/testthat/test-roxygen.R @@ -2,8 +2,8 @@ test_that("use_package_doc() compatible with roxygen_ns_append()", { create_local_package() withr::local_options(list(usethis.quiet = FALSE, crayon.enabled = FALSE)) - expect_message(use_package_doc()) - expect_message(roxygen_ns_append("test"), "Adding 'test'") + expect_snapshot(use_package_doc(), transform = scrub_testpkg) + expect_snapshot(roxygen_ns_append("test"), transform = scrub_testpkg) expect_silent(roxygen_ns_append("test")) }) diff --git a/tests/testthat/test-ui.R b/tests/testthat/test-ui-legacy.R similarity index 51% rename from tests/testthat/test-ui.R rename to tests/testthat/test-ui-legacy.R index d1bf0b2cc..6874e640a 100644 --- a/tests/testthat/test-ui.R +++ b/tests/testthat/test-ui-legacy.R @@ -1,4 +1,4 @@ -test_that("basic UI actions behave as expected", { +test_that("basic legacy UI actions behave as expected", { # suppress test silencing withr::local_options(list(usethis.quiet = FALSE)) @@ -13,10 +13,10 @@ test_that("basic UI actions behave as expected", { }) }) -test_that("UI actions respect usethis.quiet = TRUE", { +test_that("legacy UI actions respect usethis.quiet = TRUE", { withr::local_options(list(usethis.quiet = TRUE)) - expect_snapshot({ + expect_no_message({ ui_line("line") ui_todo("to do") ui_done("done") @@ -36,20 +36,3 @@ test_that("ui_silence() suppresses output", { expect_output(ui_silence(ui_line()), NA) }) - -test_that("trailing slash behaviour of ui_path()", { - withr::local_options(list(crayon.enabled = FALSE)) - # target doesn't exist so no empirical evidence that it's a directory - expect_match(ui_path("abc"), "abc'$") - - # path suggests it's a directory - expect_match(ui_path("abc/"), "abc/'$") - expect_match(ui_path("abc//"), "abc/'$") - - # path is known to be a directory - tmpdir <- withr::local_tempdir(pattern = "ui_path") - - expect_match(ui_path(tmpdir), "/'$") - expect_match(ui_path(paste0(tmpdir, "/")), "[^/]/'$") - expect_match(ui_path(paste0(tmpdir, "//")), "[^/]/'$") -}) diff --git a/tests/testthat/test-upkeep.R b/tests/testthat/test-upkeep.R index 876add3a8..f0da07051 100644 --- a/tests/testthat/test-upkeep.R +++ b/tests/testthat/test-upkeep.R @@ -49,12 +49,12 @@ test_that("upkeep bullets don't change accidentally",{ }) test_that("get extra upkeep bullets works", { - env <- env(upkeep_bullets = function() c("extra", "upkeep bullets")) + e <- new.env(parent = empty_env()) + expect_equal(upkeep_extra_bullets(e), "") + + e$upkeep_bullets <- function() c("extra", "upkeep bullets") expect_equal( - upkeep_extra_bullets(env), + upkeep_extra_bullets(e), c("* [ ] extra", "* [ ] upkeep bullets", "") ) - - env <- NULL - expect_equal(upkeep_extra_bullets(env), "") }) diff --git a/tests/testthat/test-utils-github.R b/tests/testthat/test-utils-github.R index 9a34a1715..5875e40a8 100644 --- a/tests/testthat/test-utils-github.R +++ b/tests/testthat/test-utils-github.R @@ -85,6 +85,7 @@ test_that("parse_repo_url() errors for non-GitHub remote URLs", { }) test_that("github_remote_list() works", { + local_interactive(FALSE) create_local_project() use_git() use_git_remote("origin", "https://github.com/OWNER/REPO.git") @@ -150,14 +151,49 @@ test_that("github_remotes() works", { }) # GitHub remote configuration -------------------------------------------------- -# very sparse, but you have to start somewhere! test_that("we understand the list of all possible configs", { expect_snapshot(all_configs()) }) -test_that("fork_upstream_is_not_origin_parent is detected", { - # We've already encountered this in the wild. Here's how it happens: +test_that("'no_github' is reported correctly", { + expect_snapshot(new_no_github()) +}) + +test_that("'ours' is reported correctly", { + expect_snapshot(new_ours()) +}) + +test_that("'theirs' is reported correctly", { + expect_snapshot(new_theirs()) +}) + +test_that("'fork' is reported correctly", { + expect_snapshot(new_fork()) +}) + +test_that("'maybe_ours_or_theirs' is reported correctly", { + expect_snapshot(new_maybe_ours_or_theirs()) +}) + +test_that("'maybe_fork' is reported correctly", { + expect_snapshot(new_maybe_fork()) +}) + +test_that("'fork_cannot_push_origin' is reported correctly", { + expect_snapshot(new_fork_cannot_push_origin()) +}) + +test_that("'fork_upstream_is_not_origin_parent' is reported correctly", { + expect_snapshot(new_fork_upstream_is_not_origin_parent()) +}) + +test_that("'upstream_but_origin_is_not_fork' is reported correctly", { + expect_snapshot(new_upstream_but_origin_is_not_fork()) +}) + +test_that("'fork_upstream_is_not_origin_parent' is detected correctly", { + # inspired by something that actually happened: # 1. r-pkgs/gh is created # 2. user forks and clones: origin = USER/gh, upstream = r-pkgs/gh # 3. parent repo becomes r-lib/gh, due to transfer or ownership or owner @@ -174,8 +210,24 @@ test_that("fork_upstream_is_not_origin_parent is detected", { gr$can_push <- TRUE gr$perm_known <- TRUE gr$parent_repo_owner <- c("r-lib", NA) + gr$parent_repo_name <- c("gh", NA) + gr$parent_repo_spec <- c("r-lib/gh", NA) local_mocked_bindings(github_remotes = function(...) gr) cfg <- github_remote_config() expect_equal(cfg$type, "fork_upstream_is_not_origin_parent") expect_snapshot(error = TRUE, stop_bad_github_remote_config(cfg)) }) + +test_that("bad github config error", { + expect_snapshot( + error = TRUE, + stop_bad_github_remote_config(new_fork_upstream_is_not_origin_parent()) + ) +}) + +test_that("maybe bad github config error", { + expect_snapshot( + error = TRUE, + stop_maybe_github_remote_config(new_maybe_fork()) + ) +}) diff --git a/tests/testthat/test-utils-ui.R b/tests/testthat/test-utils-ui.R new file mode 100644 index 000000000..c25a4917a --- /dev/null +++ b/tests/testthat/test-utils-ui.R @@ -0,0 +1,231 @@ +cli::test_that_cli("ui_bullets() look as expected", { + # suppress test silencing + withr::local_options(list(usethis.quiet = FALSE)) + + expect_snapshot( + ui_bullets(c( + # relate to legacy functions + "_" = "todo", # ui_todo() + "v" = "done", # ui_done() + "x" = "oops", # ui_oops() + "i" = "info", # ui_info() + "noindent", # ui_line() + + # other cli bullets that have no special connection to usethis history + " " = "indent", + "*" = "bullet", + ">" = "arrow", + "!" = "warning" + )) + ) +}) + +test_that("ui_bullets() respect usethis.quiet = TRUE", { + withr::local_options(list(usethis.quiet = TRUE)) + + expect_no_message( + ui_bullets(c( + # relate to legacy functions + "_" = "todo", # ui_todo() + "v" = "done", # ui_done() + "x" = "oops", # ui_oops() + "i" = "info", # ui_info() + "noindent", # ui_line() + + # other cli bullets that have no special connection to usethis history + " " = "indent", + "*" = "bullet", + ">" = "arrow", + "!" = "warning" + )) + ) +}) + +cli::test_that_cli("ui_bullets() does glue interpolation and inline markup", { + # suppress test silencing + withr::local_options(list(usethis.quiet = FALSE)) + + x <- "world" + + expect_snapshot( + ui_bullets(c( + "i" = "Hello, {x}!", + "v" = "Updated the {.field BugReports} field", + "x" = "Scary {.code code} or {.fun function}" + )) + ) +}) + +test_that("trailing slash behaviour of ui_path_impl()", { + # target doesn't exist so no empirical evidence that it's a directory + expect_match(ui_path_impl("abc"), "abc$") + + # path suggests it's a directory + expect_match(ui_path_impl("abc/"), "abc/$") + expect_match(ui_path_impl("abc//"), "abc/$") + + # path is known to be a directory + tmpdir <- withr::local_tempdir(pattern = "ui_path_impl") + + expect_match(ui_path_impl(tmpdir), "/$") + expect_match(ui_path_impl(paste0(tmpdir, "/")), "[^/]/$") + expect_match(ui_path_impl(paste0(tmpdir, "//")), "[^/]/$") +}) + +test_that("ui_abort() works", { + expect_usethis_error(ui_abort("spatula"), "spatula") + + # usethis.quiet should have no effect on this + withr::local_options(list(usethis.quiet = TRUE)) + expect_usethis_error(ui_abort("whisk"), "whisk") +}) + +test_that("ui_abort() defaults to 'x' for first bullet", { + expect_snapshot(error = TRUE, ui_abort("no explicit bullet")) +}) + +test_that("ui_abort() can take explicit first bullet", { + expect_snapshot(error = TRUE, ui_abort(c("v" = "success bullet"))) +}) + +test_that("ui_abort() defaults to 'i' for non-first bullet", { + expect_snapshot( + error = TRUE, + ui_abort(c( + "oops", + " " = "space bullet", + "info bullet", + "v" = "success bullet" + )) + ) +}) + +cli::test_that_cli("ui_code_snippet() with scalar input", { + withr::local_options(list(usethis.quiet = FALSE)) + + expect_snapshot( + ui_code_snippet(" + options( + warnPartialMatchArgs = TRUE, + warnPartialMatchDollar = TRUE, + warnPartialMatchAttr = TRUE + )") + ) +}, configs = c("plain", "ansi")) + +cli::test_that_cli("ui_code_snippet() with vector input", { + withr::local_options(list(usethis.quiet = FALSE)) + + expect_snapshot( + ui_code_snippet(c( + "options(", + " warnPartialMatchArgs = TRUE,", + " warnPartialMatchDollar = TRUE,", + " warnPartialMatchAttr = TRUE", + ")" + )) + ) +}, configs = c("plain", "ansi")) + +cli::test_that_cli("ui_code_snippet() when language is not R", { + withr::local_options(list(usethis.quiet = FALSE)) + h <- "blah.h" + expect_snapshot( + ui_code_snippet("#include <{h}>", language = "") + ) +}, configs = c("plain", "ansi")) + +cli::test_that_cli("ui_code_snippet() can interpolate", { + withr::local_options(list(usethis.quiet = FALSE)) + + true_val <- "TRUE" + false_val <- "'FALSE'" + + expect_snapshot( + ui_code_snippet("if (1) {true_val} else {false_val}") + ) +}, configs = c("plain", "ansi")) + +cli::test_that_cli("ui_code_snippet() can NOT interpolate", { + withr::local_options(list(usethis.quiet = FALSE)) + expect_snapshot({ + ui_code_snippet( + "foo <- function(x){x}", + interpolate = FALSE + ) + ui_code_snippet( + "foo <- function(x){{x}}", + interpolate = TRUE + ) + }) +}, configs = c("plain", "ansi")) + +test_that("bulletize() works", { + withr::local_options(list(usethis.quiet = FALSE)) + expect_snapshot(ui_bullets(bulletize(letters))) + expect_snapshot(ui_bullets(bulletize(letters, bullet = "x"))) + expect_snapshot(ui_bullets(bulletize(letters, n_show = 2))) + expect_snapshot(ui_bullets(bulletize(letters[1:6]))) + expect_snapshot(ui_bullets(bulletize(letters[1:7]))) + expect_snapshot(ui_bullets(bulletize(letters[1:8]))) + expect_snapshot(ui_bullets(bulletize(letters[1:6], n_fudge = 0))) + expect_snapshot(ui_bullets(bulletize(letters[1:8], n_fudge = 3))) +}) + +test_that("usethis_map_cli() works", { + x <- c("aaa", "bbb", "ccc") + expect_equal( + usethis_map_cli(x, template = "{.file <>}"), + c("{.file aaa}", "{.file bbb}", "{.file ccc}") + ) +}) + +cli::test_that_cli("ui_special() works", { + expect_snapshot(cli::cli_text(ui_special())) + expect_snapshot(cli::cli_text(ui_special("whatever"))) +}, configs = c("plain", "ansi")) + +cli::test_that_cli("kv_line() looks as expected in basic use", { + withr::local_options(list(usethis.quiet = FALSE)) + + expect_snapshot({ + kv_line("CHARACTER", "VALUE") + kv_line("NUMBER", 1) + kv_line("LOGICAL", TRUE) + }) +}, configs = c("plain", "fancy")) + +cli::test_that_cli("kv_line() can interpolate and style inline in key", { + withr::local_options(list(usethis.quiet = FALSE)) + + field <- "SOME_FIELD" + expect_snapshot( + kv_line("Let's reveal {.field {field}}", "whatever") + ) +}, configs = c("plain", "fancy")) + +cli::test_that_cli("kv_line() can treat value in different ways", { + withr::local_options(list(usethis.quiet = FALSE)) + + value <- "some value" + adjective <- "great" + url <- "https://usethis.r-lib.org/" + + expect_snapshot({ + # evaluation in .envir + kv_line("Key", value) + + # NULL is special + kv_line("Something we don't have", NULL) + # explicit special + kv_line("Key", ui_special("discovered")) + + # value taken at face value + kv_line("Key", "something {.emph important}") + + # I() indicates value has markup + kv_line("Key", I("something {.emph important}")) + kv_line("Key", I("something {.emph {adjective}}")) + kv_line("Interesting file", I("{.url {url}}")) + }) +}, configs = c("plain", "fancy")) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 508593554..5a9ab7c38 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -2,9 +2,13 @@ test_that("check_is_named_list() works", { l <- list(a = "a", b = 2, c = letters) expect_identical(l, check_is_named_list(l)) - expect_usethis_error(check_is_named_list(NULL), "must be a list") - expect_usethis_error(check_is_named_list(c(a = "a", b = "b")), "must be a list") - expect_usethis_error(check_is_named_list(list("a", b = 2)), "Names of .+ must be") + user_facing_function <- function(somevar) { + check_is_named_list(somevar) + } + + expect_snapshot(error = TRUE, user_facing_function(NULL)) + expect_snapshot(error = TRUE, user_facing_function(c(a = "a", b = "b"))) + expect_snapshot(error = TRUE, user_facing_function(list("a", b = 2))) }) test_that("asciify() substitutes non-ASCII but respects case", { diff --git a/vignettes/articles/ui-cli-conversion.Rmd b/vignettes/articles/ui-cli-conversion.Rmd new file mode 100644 index 000000000..b403a411e --- /dev/null +++ b/vignettes/articles/ui-cli-conversion.Rmd @@ -0,0 +1,442 @@ +--- +title: "Converting usethis's UI to use cli" +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "", + out.width = "100%", + #asciicast_knitr_output = "html", + asciicast_theme = "solarized-light", # tango, solarized-dark, solarized-light + asciicast_cols = 72 +) +asciicast::init_knitr_engine( + #echo = TRUE, + #echo_input = FALSE, + startup = quote({ + library(usethis) + library(glue) + options(cli.num_colors = 256) + ui_code_snippet <- usethis:::ui_code_snippet + usethis_theme <- usethis:::usethis_theme + ui_inform <- usethis:::ui_inform + kv_line <- usethis:::kv_line + ui_special <- usethis:::ui_special + ui_abort <- usethis:::ui_abort + ui_bullets <- usethis:::ui_bullets + set.seed(1) }) +) +``` + +```{r setup} +library(usethis) +library(glue) +``` + +*In a hidden chunk here, I'm "exporting" some unexported internal helpers, so that I can use them and talk about them. For similar reasons, I attach glue above, so that certain glue functions work here, without explicitly namespacing them.* + +```{r} +#| include: false +ui_code_snippet <- usethis:::ui_code_snippet +usethis_theme <- usethis:::usethis_theme +ui_inform <- usethis:::ui_inform +kv_line <- usethis:::kv_line +ui_special <- usethis:::ui_special +ui_abort <- usethis:::ui_abort +ui_bullets <- usethis:::ui_bullets +``` + +## Block styles + +The block styles exist to produce bulleted output with a specific symbol, using a specific color. + +```{asciicast} +#| collapse: false +f <- function() { + ui_todo("ui_todo(): red bullet") + ui_done("ui_done(): green check") + ui_oops("ui_oops(): red x") + ui_info("ui_info(): yellow i") + ui_line("ui_line(): (no symbol)") +} +f() +``` + +Another important feature is that all of this output can be turned off package-wide via the `usethis.quiet` option. + +```{asciicast} +withr::with_options( + list(usethis.quiet = TRUE), + ui_info("You won't see this message.") +) +withr::with_options( + list(usethis.quiet = FALSE), # this is the default + ui_info("But you will see this one.") +) +``` + +These styles are very close to what can be done with `cli::cli_bullets()` and the way it responds to the names of its input `text`. + +```{asciicast} +cli::cli_bullets(c( + "noindent", + " " = "indent", + "*" = "bullet", + ">" = "arrow", + "v" = "success", + "x" = "danger", + "!" = "warning", + "i" = "info" +)) +``` + +A direct translation would look something like this: + +| Legacy `ui_*()` | `cli_bullets()` shortcode | tweaks needed | +|-----------------|---------------------------|-------------------------------| +| `ui_todo()` | `*` | blue (default) -\> red | +| `ui_done()` | `v` | perfect match | +| `ui_oops()` | `x` | perfect match | +| `ui_info()` | `i` | blue (default) -\> yellow | +| `ui_line()` | (unnamed) | sort of a perfect match? although sometimes `ui_line()` is used just to get a blank line | + +The overall conversion plan is to switch to a new function, `ui_bullets()`, which is a wrapper around `cli::cli_bullets()`, that adds a few features: + +* Early exit, without emitting messages, if the `usethis.quiet` option is `TRUE`. +* A usethis theme, that changes the color of certain bullet styles and adds a new style for todo's. + +```{asciicast} +ui_bullets(c( + "v" = "A great success!", + "_" = "Something you need to do.", + "x" = "Bad news.", + "i" = "The more you know.", + " " = "I'm just here for the indentation.", + "No indentation at all. Not used much in usethis." +)) +``` + + +Summary of what I've done for todo's: + +- Introduce a new bullet shortcode for a todo. Proposal: `_` (instead of `*`), which seems to be the best single ascii character that evokes a place to check something off. +- Use `cli::symbol$checkbox_off` as the symbol (instead of a generic bullet). I guess it will continue to be red. + +In terms of the block styles, that just leaves `ui_code_block()`, which is pretty different. +`ui_code_block()` is used to put some code on the screen and optionally place it on the clipboard. +I have created a new function, `ui_code_snippet()` that is built around `cli::code_block()`. +Main observations: + +* `cli::code_block(language = "R")` applies syntax highlighting and hyperlinks (e.g. to function help topics) to R code, which is cool. Therefore the `language` argument is also exposed in `ui_code_snippet()`, defaulting to `"R"`. Use `""` for anything that's not R code: + ```{asciicast} + ui_code_snippet("x <- 1 + 2") + ui_code_snippet("#include ", language = "") + ``` +* `ui_code_snippet()` takes a scalar glue-type template string or a vector of lines. Note that the two calls below produce the same output. + ```{asciicast} + ui_code_snippet(" + options( + warnPartialMatchArgs = TRUE, + warnPartialMatchDollar = TRUE, + warnPartialMatchAttr = TRUE + )") + # produces same result as + ui_code_snippet(c( + "options(", + " warnPartialMatchArgs = TRUE,", + " warnPartialMatchDollar = TRUE,", + " warnPartialMatchAttr = TRUE", + ")")) + ``` +* `ui_code_snippet()` does glue interpolation, by default, before calling `cli::cli_code()`, which means you have to think about your use of `{` and `}`. If you want literal `{` or `}`: + - Use `interpolate = FALSE`, if you don't need interpolation. + - Do the usual glue thing and double them, i.e. `{{` or `}}`. + - If this becomes a real pain, open an issue/PR about adding `.open` and `.close` as arguments to `ui_code_snippet()`. + +## Utility functions + +The block style functions all route through some unexported utility functions. + +`is_quiet()` just consults the `usethis.quiet` option and implements the default of `FALSE`. + +```{r} +#| eval: false +is_quiet <- function() { + isTRUE(getOption("usethis.quiet", default = FALSE)) +} +``` + +`ui_bullet()` is an intermediate helper used by `ui_todo()`, `ui_done()`, `ui_oops()` and `ui_info()`. +It does some hygiene related to indentation (using the `indent()` utility function), then calls `ui_inform()`. +`ui_line()` and `ui_code()` both call `ui_inform()` directly. + +`ui_inform()` is just a wrapper around `rlang::inform()` that is guarded by a call to `is_quiet()` + +```{r} +#| eval: false +ui_inform <- function(...) { + if (!is_quiet()) { + inform(paste0(...)) + } + invisible() +} +``` + +Other than `is_quiet()`, which will continue to play the same role, I anticipate that we no longer need these utilities (`indent()`, `ui_bullet()`, `ui_inform()`). +Updates from the future: + +* `indent()` turns out to still be useful in `ui_code_snippet()`, so I've inlined + it there, to avoid any reliance on definitions in ui-legacy.R. +* `ui_bullet()` has been renamed to `ui_legacy_bullet()` for auto-completion happiness with the new `ui_bullets()`. + +Let's cover `ui_silence()` while we're here, which *is* exported. +It's just a `withr::with_*()` function for executing `code` with `usethis.quiet = TRUE`. + +```{r} +#| eval: false +ui_silence <- function(code) { + withr::with_options(list(usethis.quiet = TRUE), code) +} +``` + +## Inline styles + +### Legacy functions + +usethis has its own inline styles (mostly) for use inside functions like `ui_todo()`: + +* `ui_field()` +* `ui_value()` +* `ui_path()` +* `ui_code()` +* `ui_unset()` + +```{asciicast} +# why is this block truncated from the top in the rendered document? +new_val <- "oxnard" +x <- glue("{ui_field('name')} set to {ui_value(new_val)}") +dput(x) +ui_done(x) +``` + +The inline styles enact some combination of: + +* Color, e.g. `crayon::green(x)` +* Collapsing a collection of things to one thing, e.g. `c("a", "b", "c")` to "a, b, c" +* Quoting, e.g. `encodeString(x, quote = "'")` + +`ui_path()` is special because it potentially modifies the input before styling it. +`ui_path()` first makes the path relative to a specific base (by default, the active project root) and, if the path is a directory, it also ensures there is a trailing `/`. + +`ui_unset()` is a special purpose helper used when we need to report that something is unknown, not configured, nonexistent, etc. + +```{asciicast} +# why is this block truncated from the top in the rendered document? +x <- glue("Your super secret password is {ui_unset()}.") +dput(x) +ui_info(x) +``` + +### cli replacements + +In general, we can move towards cli's native inline-markup: + + +Here's the general conversion plan: + +- `ui_field()` becomes `{.field blah}`. In `usethis_theme()`, I tweak the `.field` style to apply single quotes if color is not available, which is what `ui_field()` has always done. +- `ui_value()` becomes `{.val value}`. +- `ui_path()` is connected to `{.path path/to/thing}`, but, as explained above, `ui_path()` also does more. Therefore, I abstracted the "path math" into an internal helper, `ui_path_impl()`, which is aliased to `pth()` for compactness. Here's a typical conversion: + ```{r} + #| eval: false + # using legacy functions + ui_done("Setting {ui_field('LazyData')} to \\ + {ui_value('true')} in {ui_path('DESCRIPTION')}") + # using new cli-based ui + ui_bullets(c( + "v" = "Setting {.field LazyData} to {.val true} in {.path {pth('DESCRIPTION')}}." + )) + ``` + It would be nice to create a custom inline class, e.g. `{.ui_path {some_path}}`, which I have done in, e.g., googledrive. + But it's not easy to do this *while still inheriting cli's `file:` hyperlink behaviour*, which is very desirable. + So that leads to the somewhat clunky, verbose pattern above, but it gives a nice result. +- `ui_code()` gets replaced by various inline styles, depending on what the actual goal is, such as: + - `{.code some_text}` + - `{.arg some_argument}` + - `{.cls some_class}` + - `{.fun devtools::build_readme}` + - `{.help some_function}` + - `{.run usethis::usethis_function()}` + - `{.topic some_topic}` + - `{.var some_variable}` +- `ui_unset()` is replaced by `ui_special()`, which you'll see more of below. Currently the intended grey color doesn't show up when I render this document using solarized-dark and so far I can't get to the bottom of that :( Why isn't it the same grey as "[Copied to clipboard]" in `ui_code_snippet()`, which does work? + +## Conditions + +I'm moving from `ui_stop()`: + +```{r} +#| eval: false +ui_stop <- function(x, .envir = parent.frame()) { + x <- glue_collapse(x, "\n") + x <- glue(x, .envir = .envir) + + cnd <- structure( + class = c("usethis_error", "error", "condition"), + list(message = x) + ) + + stop(cnd) +} +``` + +to `ui_abort()`: + +```{r} +#| eval: false +ui_abort <- function(message, ..., class = NULL, .envir = parent.frame()) { + cli::cli_div(theme = usethis_theme()) + # bullet naming gymnastics, see below + cli::cli_abort( + message, + class = c(class, "usethis_error"), + .envir = .envir, + ... + ) +} +``` + +The main point of `ui_abort()` is to use to `cli_abort()` (and to continue applying the `"usethis_error"` class). + +I also use `ui_abort()` to apply different default bullet naming/styling. +Starting with `"x"` and then defaulting to `"i"` seems to fit best with usethis's existing errors. + +```{asciicast} +#| error: true +# why is this block truncated from the top in the rendered document? +block_start = "# <<<" +block_end = "# >>>" +ui_abort(c( + "Invalid block specification.", + "Must start with {.code {block_start}} and end with {.code {block_end}}." +)) +``` + +Any bullets that are explicitly given are honored. + +```{asciicast} +#| error: true +ui_abort(c("v" = "It's weird to give a green check in an error, but whatever.")) +ui_abort(c( + "!" = "Things are not ideal.", + ">" = "Look at me!" +)) +``` + +`rlang::abort()` and `cli::cli_abort()` start with `"!"` by default, then use `"*"` and `" "`, respectively. + +The legacy functions also include `ui_warn()`. +It has very little usage and, instead of converting it, I've eliminated its use altogether in favor of a `"!"` bullet: + +```{asciicast} +ui_bullets(c("!" = "The guy she told you not to worry about.")) +``` + +Sidebar: Now that I'm looking at a lot of the new errors with `ui_abort()` I realize that usethis also needs to be passing the `call` argument along. +I'm going to leave that for a future, separate effort. + +## Sitrep and format helpers + +This is a small clump of functions that support sitrep-type output. + +- `hd_line()` *unexported and, apparently, unused! now removed* +- `kv_line()` *unexported, so has new cli implementation* +- `ui_unset()` *exported and succeeded by `ui_special()`* + +`kv_line()` stands for "key-value line". +Here's what it used to be: + +```{asciicast} +kv_line_legacy <- function(key, value, .envir = parent.frame()) { + value <- if (is.null(value)) ui_unset() else ui_value(value) + key <- glue(key, .envir = .envir) + ui_inform(glue("{cli::symbol$bullet} {key}: {value}")) +} + +url <- "https://github.com/r-lib/usethis.git" +remote <- "origin" +kv_line_legacy("URL for the {ui_value(remote)} remote", url) + +host <- "github.com" +kv_line_legacy("Personal access token for {ui_value(host)}", NULL) +``` + +Key features: + +* Interpolates data and applies inline style to the result. + Works differently for `key` and `value`, because you're much more likely to use interpolation and styling in `key` than `value`. +* Has special handling when `value` is `NULL`. +* Applies `"*"` bullet name/style to over all result. + +I won't show the updated source for `kv_line()` but here is some usage to show what it's capable of: + +```{asciicast} +# why is this block truncated from the top in the rendered document? +noun <- "thingy" +value <- "VALUE" +kv_line("Let's reveal {.field {noun}}", "whatever") + +kv_line("URL for the {.val {remote}} remote", I("{.url {url}}")) + +kv_line("Personal access token for {.val {host}}", NULL) + +kv_line("Personal access token for {.val {host}}", ui_special("discovered")) +``` + +`ui_special()` is the successor to `ui_unset()`. + +## Questions + +There's currently no drop-in substitute for `ui_yeah()` and `ui_nope()` in cli. +Related issues: , . +Therefore, in the meantime, `ui_yeah()` and `ui_nope()` are not-quite-superseded for external users. + +However, internally, I've switched to the unexported functions `ui_yep()` and `ui_nah()` that are lightly modified versions of `ui_yeah()` and `ui_nope()` that use cli for styling. + +```{r} +#| eval: false +if (ui_nope(" + Current branch ({ui_value(actual)}) is not repo's default \\ + branch ({ui_value(default_branch)}).{details}")) { + ui_abort("Cancelling. Not on desired branch.") + } +``` + +## Miscellaneous notes + +* I've been adding a period to the end of messages, as a general rule. +* In terms of whitespace and indentation, I've settled on some conventions. + The overall goal is to get the right user-facing output (obviously), while making it as easy as possible to _predict_ what that's going to look like when you're writing the code. + ```{r} + #| eval: false + ui_bullets(c( + "i" = "Downloading into {.path {pth(destdir)}}.", + "_" = "Prefer a different location? Cancel, try again, and specify + {.arg destdir}." + )) + ... + ui_bullets(c("x" = "Things are very wrong.")) + ``` + Key points: + - Put `ui_bullets(c(` on its own line, then all of the bullet items, followed by `))` on its own line. Sometimes I make an exception for a bullet list with exactly one, short bullet item. + - Use hard line breaks inside bullet text to comply with surrounding line length. In subsequent lines, use indentation to get you just past the opening `"`. This extraneous white space is later rationalized by cli, which handles wrapping. + - Surround bullet names like `x` and `i` with quotes, even though you don't have to, because it's required for other names, such as `!` or `_` and it's better to be consistent. + - Here's another style I like that applies to `ui_abort()`, where there's just one, unnamed bullet, but the call doesn't fit on one line. + ```{r} + #| eval: false + pr <- list(pr_number = 13) + ui_abort(" + The repo or branch where PR #{pr$pr_number} originates seems to have been + deleted.") + ```