From d1fa7d9a807d97e00c891916bf9a10fed7932a12 Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Mon, 22 Jun 2026 13:40:09 -0400 Subject: [PATCH 1/6] Add pkg_detail RPC for packages pane detail editor --- .../ark/src/modules/positron/packages_pane.R | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/ark/src/modules/positron/packages_pane.R b/crates/ark/src/modules/positron/packages_pane.R index c961570739..afd0051973 100644 --- a/crates/ark/src/modules/positron/packages_pane.R +++ b/crates/ark/src/modules/positron/packages_pane.R @@ -115,6 +115,64 @@ as.list(version) } +# Return detail fields for a single installed package by name. +#' @export +.ps.rpc.pkg_detail <- function(name) { + installed <- rownames(utils::installed.packages()) + if (!nzchar(name) || !(name %in% installed)) { + return(NULL) + } + fields <- c( + "Title", "Author", "Maintainer", "License", + "Depends", "Imports", "LinkingTo", "Repository", "Date/Publication" + ) + d <- utils::packageDescription(name, fields = fields) + + # Collapse DCF whitespace; return NULL for missing (NA) fields. + clean <- function(x) { + if (is.null(x) || is.na(x)) { + return(NULL) + } + trimws(gsub("\\s+", " ", x, perl = TRUE)) + } + + # Parse a comma-separated dependency field into bare package names + # (strip version constraints like "(>= 1.0)"). + parse_deps <- function(x) { + if (is.null(x) || is.na(x)) { + return(character(0)) + } + parts <- trimws(strsplit(x, ",")[[1]]) + names <- trimws(sub("\\(.*\\)", "", parts)) + names[nzchar(names)] + } + + base_pkgs <- rownames(utils::installed.packages(priority = "base")) + deps <- unique(c(parse_deps(d$Depends), parse_deps(d$Imports), parse_deps(d$LinkingTo))) + deps <- setdiff(deps, c("R", base_pkgs)) + + out <- list(name = name, dependencyCount = length(deps)) + + # Prefer Maintainer for author display; fall back to Author. + author <- clean(d$Maintainer) + if (is.null(author)) { + author <- clean(d$Author) + } + title <- clean(d$Title) + license <- clean(d$License) + repo <- clean(d$Repository) + published <- clean(d[["Date/Publication"]]) + + if (!is.null(title)) out$title <- title + if (!is.null(author)) out$author <- author + if (!is.null(license)) out$license <- license + if (!is.null(repo)) out$sourceRepository <- repo + if (!is.null(published)) out$publishedDate <- published + + out +} + + # Return the list of outdated packages with their latest available versions. # # `utils::old.packages()` queries the user's configured repositories, so From 05a39e45d476dbbf287ddca69da37feb4b41af2f Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Tue, 23 Jun 2026 15:45:10 -0400 Subject: [PATCH 2/6] Apply air formatting to pkg_detail RPC --- .../ark/src/modules/positron/packages_pane.R | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/ark/src/modules/positron/packages_pane.R b/crates/ark/src/modules/positron/packages_pane.R index afd0051973..1f81ec0c9b 100644 --- a/crates/ark/src/modules/positron/packages_pane.R +++ b/crates/ark/src/modules/positron/packages_pane.R @@ -123,8 +123,15 @@ return(NULL) } fields <- c( - "Title", "Author", "Maintainer", "License", - "Depends", "Imports", "LinkingTo", "Repository", "Date/Publication" + "Title", + "Author", + "Maintainer", + "License", + "Depends", + "Imports", + "LinkingTo", + "Repository", + "Date/Publication" ) d <- utils::packageDescription(name, fields = fields) @@ -148,7 +155,11 @@ } base_pkgs <- rownames(utils::installed.packages(priority = "base")) - deps <- unique(c(parse_deps(d$Depends), parse_deps(d$Imports), parse_deps(d$LinkingTo))) + deps <- unique(c( + parse_deps(d$Depends), + parse_deps(d$Imports), + parse_deps(d$LinkingTo) + )) deps <- setdiff(deps, c("R", base_pkgs)) out <- list(name = name, dependencyCount = length(deps)) @@ -163,11 +174,21 @@ repo <- clean(d$Repository) published <- clean(d[["Date/Publication"]]) - if (!is.null(title)) out$title <- title - if (!is.null(author)) out$author <- author - if (!is.null(license)) out$license <- license - if (!is.null(repo)) out$sourceRepository <- repo - if (!is.null(published)) out$publishedDate <- published + if (!is.null(title)) { + out$title <- title + } + if (!is.null(author)) { + out$author <- author + } + if (!is.null(license)) { + out$license <- license + } + if (!is.null(repo)) { + out$sourceRepository <- repo + } + if (!is.null(published)) { + out$publishedDate <- published + } out } From aea69898c3c6278c8efc0028cb319fbfea3dd893 Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Thu, 25 Jun 2026 13:46:46 -0400 Subject: [PATCH 3/6] Show only the primary license in pkg_detail --- crates/ark/src/modules/positron/packages_pane.R | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/ark/src/modules/positron/packages_pane.R b/crates/ark/src/modules/positron/packages_pane.R index 1f81ec0c9b..291313aeab 100644 --- a/crates/ark/src/modules/positron/packages_pane.R +++ b/crates/ark/src/modules/positron/packages_pane.R @@ -154,6 +154,20 @@ names[nzchar(names)] } + # Reduce an R License field to its primary license: the first alternative + # (before "|"), without the "+ file LICENSE" clause. + primary_license <- function(x) { + if (is.null(x)) { + return(NULL) + } + first <- trimws(strsplit(x, "\\|")[[1]][1]) + first <- trimws(sub("\\s*\\+\\s*file\\s+.*$", "", first, ignore.case = TRUE)) + if (!nzchar(first)) { + return(NULL) + } + first + } + base_pkgs <- rownames(utils::installed.packages(priority = "base")) deps <- unique(c( parse_deps(d$Depends), @@ -170,7 +184,7 @@ author <- clean(d$Author) } title <- clean(d$Title) - license <- clean(d$License) + license <- primary_license(clean(d$License)) repo <- clean(d$Repository) published <- clean(d[["Date/Publication"]]) From 62a917619e9288fabe8728a9120d92984f1ea0df Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Thu, 25 Jun 2026 15:22:27 -0400 Subject: [PATCH 4/6] Wrap sub() call to satisfy air formatting --- crates/ark/src/modules/positron/packages_pane.R | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/ark/src/modules/positron/packages_pane.R b/crates/ark/src/modules/positron/packages_pane.R index 291313aeab..0630bf84c3 100644 --- a/crates/ark/src/modules/positron/packages_pane.R +++ b/crates/ark/src/modules/positron/packages_pane.R @@ -161,7 +161,12 @@ return(NULL) } first <- trimws(strsplit(x, "\\|")[[1]][1]) - first <- trimws(sub("\\s*\\+\\s*file\\s+.*$", "", first, ignore.case = TRUE)) + first <- trimws(sub( + "\\s*\\+\\s*file\\s+.*$", + "", + first, + ignore.case = TRUE + )) if (!nzchar(first)) { return(NULL) } From 161f556e17888b56775c4ef931b3b13b6ba8f3ac Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Fri, 26 Jun 2026 15:23:49 -0400 Subject: [PATCH 5/6] Remove dependency count from pkg_detail --- .../ark/src/modules/positron/packages_pane.R | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/crates/ark/src/modules/positron/packages_pane.R b/crates/ark/src/modules/positron/packages_pane.R index 0630bf84c3..6503066adc 100644 --- a/crates/ark/src/modules/positron/packages_pane.R +++ b/crates/ark/src/modules/positron/packages_pane.R @@ -143,17 +143,6 @@ trimws(gsub("\\s+", " ", x, perl = TRUE)) } - # Parse a comma-separated dependency field into bare package names - # (strip version constraints like "(>= 1.0)"). - parse_deps <- function(x) { - if (is.null(x) || is.na(x)) { - return(character(0)) - } - parts <- trimws(strsplit(x, ",")[[1]]) - names <- trimws(sub("\\(.*\\)", "", parts)) - names[nzchar(names)] - } - # Reduce an R License field to its primary license: the first alternative # (before "|"), without the "+ file LICENSE" clause. primary_license <- function(x) { @@ -173,15 +162,7 @@ first } - base_pkgs <- rownames(utils::installed.packages(priority = "base")) - deps <- unique(c( - parse_deps(d$Depends), - parse_deps(d$Imports), - parse_deps(d$LinkingTo) - )) - deps <- setdiff(deps, c("R", base_pkgs)) - - out <- list(name = name, dependencyCount = length(deps)) + out <- list(name = name) # Prefer Maintainer for author display; fall back to Author. author <- clean(d$Maintainer) From 9584e5f50c9a229fa0373046d820e53c3ec4fd67 Mon Sep 17 00:00:00 2001 From: Brice Stacey Date: Mon, 29 Jun 2026 15:39:55 -0400 Subject: [PATCH 6/6] Read pkg_detail straight from packageDescription Drop the installed.packages() existence check in favor of calling packageDescription() directly, which returns a length-1 NA when the package isn't installed. installed.packages() parses every installed package's DESCRIPTION, so it is needlessly expensive for a single-package lookup. Also simplify the result assembly with Filter(Negate(is.null), ...) and drop the now-unused Depends/Imports/LinkingTo fields. --- .../ark/src/modules/positron/packages_pane.R | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/crates/ark/src/modules/positron/packages_pane.R b/crates/ark/src/modules/positron/packages_pane.R index 6503066adc..d2362b24fd 100644 --- a/crates/ark/src/modules/positron/packages_pane.R +++ b/crates/ark/src/modules/positron/packages_pane.R @@ -118,8 +118,7 @@ # Return detail fields for a single installed package by name. #' @export .ps.rpc.pkg_detail <- function(name) { - installed <- rownames(utils::installed.packages()) - if (!nzchar(name) || !(name %in% installed)) { + if (!nzchar(name)) { return(NULL) } fields <- c( @@ -127,13 +126,17 @@ "Author", "Maintainer", "License", - "Depends", - "Imports", - "LinkingTo", "Repository", "Date/Publication" ) - d <- utils::packageDescription(name, fields = fields) + # Read straight from the package's DESCRIPTION. packageDescription() returns + # a length-1 NA (and warns) when the package isn't installed, which is far + # cheaper than installed.packages() -- that parses every installed package's + # DESCRIPTION just to answer an existence check. + d <- suppressWarnings(utils::packageDescription(name, fields = fields)) + if (!inherits(d, "packageDescription")) { + return(NULL) + } # Collapse DCF whitespace; return NULL for missing (NA) fields. clean <- function(x) { @@ -162,35 +165,22 @@ first } - out <- list(name = name) - # Prefer Maintainer for author display; fall back to Author. author <- clean(d$Maintainer) if (is.null(author)) { author <- clean(d$Author) } - title <- clean(d$Title) - license <- primary_license(clean(d$License)) - repo <- clean(d$Repository) - published <- clean(d[["Date/Publication"]]) - - if (!is.null(title)) { - out$title <- title - } - if (!is.null(author)) { - out$author <- author - } - if (!is.null(license)) { - out$license <- license - } - if (!is.null(repo)) { - out$sourceRepository <- repo - } - if (!is.null(published)) { - out$publishedDate <- published - } - out + # Drop absent (NULL) fields so the other side only sees populated keys. + out <- list( + name = name, + title = clean(d$Title), + author = author, + license = primary_license(clean(d$License)), + sourceRepository = clean(d$Repository), + publishedDate = clean(d[["Date/Publication"]]) + ) + Filter(Negate(is.null), out) }