From 05afac4a453fc0f5d264366984a774f93b6554e6 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 02:29:32 +0000 Subject: [PATCH 1/8] feat: add native plot backend --- R/profile.R | 3 +- README.md | 6 +- package.json | 10 +- sess/DESCRIPTION | 2 +- sess/R/hooks.R | 78 ++++++++-- sess/R/server.R | 42 ++++-- sess/README.md | 32 ++-- sess/inst/tinytest/test-connect-failure.R | 2 +- sess/inst/tinytest/test-connection-message.R | 2 +- sess/inst/tinytest/test-ipc.R | 16 +- sess/inst/tinytest/test-native-plot.R | 148 +++++++++++++++++++ sess/inst/tinytest/test-plot-notification.R | 2 +- sess/inst/tinytest/test-reconnect.R | 17 ++- sess/man/connect.Rd | 16 +- sess/man/register_hooks.Rd | 13 +- sess/man/runtime_start.Rd | 3 +- src/api.d.ts | 2 +- src/plotViewer/index.ts | 12 +- src/session.ts | 9 +- src/test/suite/plotBackend.test.ts | 20 ++- src/test/suite/sessInstall.test.ts | 5 +- src/test/suite/session.test.ts | 21 ++- src/test/suite/terminal.test.ts | 19 +++ 23 files changed, 398 insertions(+), 82 deletions(-) create mode 100644 sess/inst/tinytest/test-native-plot.R diff --git a/R/profile.R b/R/profile.R index 154ae87f5..b2ed84b39 100644 --- a/R/profile.R +++ b/R/profile.R @@ -29,8 +29,7 @@ if (requireNamespace("sess", quietly = TRUE)) { plot_backend <- Sys.getenv("SESS_PLOT_BACKEND", "auto") sess::connect( use_rstudioapi = as.logical(Sys.getenv("SESS_RSTUDIOAPI", "TRUE")), - use_httpgd = (plot_backend %in% c("auto", "httpgd")), - use_jgd = (plot_backend %in% c("auto", "jgd")) + plot_backend = plot_backend ) }) } diff --git a/README.md b/README.md index 7b3a65d9a..0d61a0dae 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,16 @@ For example, assuming that you have installed `arf` and `jgd`, your user `settin { "r.consolePath": "arf", "r.bracketedPaste": true, // recommended for arf (and radian) - "r.plot.backend": "jgd" // "auto" already prefers jgd, then httpgd + "r.plot.backend": "jgd" // "auto" prefers httpgd, then jgd } ``` Please consult the relevant installation wiki pages for your OS ([Windows](https://github.com/REditorSupport/vscode-R/wiki/Installation:-Windows) | [macOS](https://github.com/REditorSupport/vscode-R/wiki/Installation:-macOS) | [Linux](https://github.com/REditorSupport/vscode-R/wiki/Installation:-Linux)) for more detailed instructions. +Set `r.plot.backend` to `"native"` to use R's configured graphics device without +opening the VS Code plot viewer automatically. The `"standard"` backend continues +to display static plots in VS Code. + ## Features * Snippets for R and R Markdown. diff --git a/package.json b/package.json index 57591a8ef..488444c8e 100644 --- a/package.json +++ b/package.json @@ -1871,15 +1871,17 @@ "auto", "standard", "httpgd", - "jgd" + "jgd", + "native" ], "markdownEnumDescriptions": [ - "Automatic: tries JGD first (if installed), then httpgd, then standard. Existing `#r.plot.useHttpgd#` configurations remain supported.", + "Automatic: tries httpgd first (if installed), then JGD, then standard. Existing `#r.plot.useHttpgd#` configurations remain supported.", "Standard static plot viewer (PNG/SVG)", "httpgd-based interactive plot viewer (requires `httpgd` R package)", - "JGD-based interactive plot viewer (requires `jgd` R package)" + "JGD-based interactive plot viewer (requires `jgd` R package)", + "Use R's configured graphics device without plot integration or an automatic VS Code plot viewer" ], - "markdownDescription": "The canonical setting for selecting the plot backend.\n\nWhen set to `auto`, the best available backend is used (JGD if installed, then httpgd, then standard). For backwards compatibility, setting `#r.plot.useHttpgd#` to `true` selects httpgd while this setting remains `auto`; workspace settings take precedence over user settings, with an explicit backend value preferred within the same scope." + "markdownDescription": "The canonical setting for selecting the plot backend. `standard` uses the VS Code static plot viewer; `native` leaves R's configured graphics device untouched.\n\nWhen set to `auto`, the best available backend is used (httpgd if installed, then JGD, then standard). For backwards compatibility, setting `#r.plot.useHttpgd#` to `true` selects httpgd while this setting remains `auto`; workspace settings take precedence over user settings, with an explicit backend value preferred within the same scope." }, "r.plot.jgd.historyLimit": { "type": "number", diff --git a/sess/DESCRIPTION b/sess/DESCRIPTION index 4aa6dc423..96b56fea0 100644 --- a/sess/DESCRIPTION +++ b/sess/DESCRIPTION @@ -1,7 +1,7 @@ Package: sess Type: Package Title: High-Performance IPC Bridge for R Sessions -Version: 3.0.1 +Version: 3.0.9000.9000 Authors@R: c( person(given = "Randy", family = "Lai", diff --git a/sess/R/hooks.R b/sess/R/hooks.R index 116c98cb3..749d616d0 100644 --- a/sess/R/hooks.R +++ b/sess/R/hooks.R @@ -1,11 +1,54 @@ #' Register VS Code runtime integrations #' #' @param use_rstudioapi Logical. Enable rstudioapi emulation. -#' @param use_httpgd Logical. Enable httpgd plot device if available. -#' @param use_jgd Logical. Enable jgd plot device if available. +#' @param use_httpgd Deprecated. Logical. Enable httpgd plot device if available. +#' Use `plot_backend` instead. +#' @param use_jgd Deprecated. Logical. Enable jgd plot device if available. +#' Use `plot_backend` instead. +#' @param plot_backend Plot backend: `auto`, `jgd`, `httpgd`, `standard`, or +#' `native`. NULL also selects `auto`. Deprecated flags select the backend +#' only when this argument is omitted. #' @export -register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE) { - runtime_start(use_rstudioapi, use_httpgd, use_jgd) +register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE, + plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) { + has_httpgd <- !missing(use_httpgd) + has_jgd <- !missing(use_jgd) + .warn_deprecated_plot_args(has_httpgd, has_jgd) + backend <- if (missing(plot_backend) && (has_httpgd || has_jgd)) { + .legacy_plot_backend(use_httpgd, use_jgd) + } else { + .resolve_plot_backend(plot_backend) + } + runtime_start(use_rstudioapi, backend) +} + +.warn_deprecated_plot_args <- function(has_httpgd, has_jgd) { + old_args <- c(if (has_httpgd) "use_httpgd", if (has_jgd) "use_jgd") + if (length(old_args)) { + warning("[sess] ", paste(old_args, collapse = " and "), + if (length(old_args) == 1L) " is deprecated; use plot_backend instead." else + " are deprecated; use plot_backend instead.", call. = FALSE) + } + invisible(NULL) +} + +.resolve_plot_backend <- function(plot_backend) { + if (is.null(plot_backend)) return("auto") + match.arg(plot_backend, c("auto", "jgd", "httpgd", "standard", "native")) +} + +.legacy_plot_backend <- function(use_httpgd, use_jgd) { + if (is.na(use_httpgd)) use_httpgd <- TRUE + if (is.na(use_jgd)) use_jgd <- FALSE + if (use_jgd && use_httpgd) "auto" else if (use_jgd) "jgd" else + if (use_httpgd) "httpgd" else "standard" +} + +.select_plot_backend <- function(plot_backend, has_httpgd, has_jgd) { + if (plot_backend == "native") return("native") + if (plot_backend %in% c("auto", "httpgd") && has_httpgd) return("httpgd") + if (plot_backend %in% c("auto", "jgd") && has_jgd) return("jgd") + "standard" } # Send runtime notifications after task callbacks return, so transport failure @@ -31,7 +74,9 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F #' Start the VS Code runtime integration (internal) #' #' @keywords internal -runtime_start <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE) { +runtime_start <- function(use_rstudioapi = TRUE, + plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) { + plot_backend <- match.arg(plot_backend) .sess_env$runtime_start_phase <- "initialize" state <- .runtime_state() if (isTRUE(state$active)) { @@ -168,9 +213,14 @@ runtime_start <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FA } invisible(x) } - # 4. Plot device: JGD > httpgd > Standard + # 4. Plot device: httpgd > JGD > Standard, or no plot integration for native .sess_env$runtime_start_phase <- "plot" - if (use_jgd && nzchar(Sys.getenv("JGD_SOCKET")) && requireNamespace("jgd", quietly = TRUE)) { + has_httpgd <- plot_backend %in% c("auto", "httpgd") && + requireNamespace("httpgd", quietly = TRUE) + has_jgd <- (plot_backend == "jgd" || (plot_backend == "auto" && !has_httpgd)) && + nzchar(Sys.getenv("JGD_SOCKET")) && requireNamespace("jgd", quietly = TRUE) + selected_backend <- .select_plot_backend(plot_backend, has_httpgd, has_jgd) + if (selected_backend == "jgd") { .runtime_set_option("device", function(...) { jgd::jgd() .runtime_track_device() @@ -202,26 +252,26 @@ runtime_start <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FA invisible(TRUE) } reconnect_jgd_device() - } else if (use_httpgd && requireNamespace("httpgd", quietly = TRUE)) { + } else if (selected_backend == "httpgd") { .runtime_set_option("device", function(...) { httpgd::hgd(silent = TRUE) .runtime_track_device() notify_client("httpgd", list(url = httpgd::hgd_url())) }) - } else { + } else if (selected_backend == "standard") { # If a specific interactive backend was explicitly requested but is # unavailable, warn before silently degrading to the standard viewer. - # (use_jgd && use_httpgd means "auto", which is meant to degrade quietly.) - if (xor(use_jgd, use_httpgd)) { - if (use_jgd && !requireNamespace("jgd", quietly = TRUE)) { + # Auto is meant to degrade quietly. + if (plot_backend %in% c("jgd", "httpgd")) { + if (plot_backend == "jgd" && !requireNamespace("jgd", quietly = TRUE)) { warning("[sess] Plot backend \"jgd\" was requested but the jgd package ", "is not installed. Falling back to the standard plot viewer. ", "Install jgd, or change the r.plot.backend setting.", call. = FALSE) - } else if (use_jgd) { + } else if (plot_backend == "jgd") { warning("[sess] Plot backend \"jgd\" was requested but no renderer ", "connection is available. Falling back to the standard plot ", "viewer.", call. = FALSE) - } else if (use_httpgd) { + } else if (plot_backend == "httpgd") { warning("[sess] Plot backend \"httpgd\" was requested but the httpgd ", "package is not installed. Falling back to the standard plot ", "viewer. Install httpgd, or change the r.plot.backend setting.", diff --git a/sess/R/server.R b/sess/R/server.R index 434556c89..0804e5ae0 100644 --- a/sess/R/server.R +++ b/sess/R/server.R @@ -3,16 +3,32 @@ #' @param endpoint Character. Local named pipe / Unix domain socket endpoint. #' If NULL, uses SESS_ENDPOINT, then SESS_DISCOVERY_FILE. #' @param use_rstudioapi Logical. Enable rstudioapi emulation. Defaults to TRUE. -#' @param use_httpgd Logical. Use httpgd for plotting if available. Defaults to TRUE. -#' @param use_jgd Logical. Use jgd for plotting if available. Defaults to FALSE. +#' @param use_httpgd Deprecated. Logical. Use httpgd for plotting if available. +#' Defaults to TRUE. Use `plot_backend` instead. +#' @param use_jgd Deprecated. Logical. Use jgd for plotting if available. +#' Defaults to FALSE. Use `plot_backend` instead. +#' @param plot_backend Plot backend: `auto`, `jgd`, `httpgd`, `standard`, or +#' `native`. NULL also selects `auto`. Deprecated flags select the backend +#' only when this argument is omitted. #' @details When SESS_DISCOVERY_FILE describes the connected endpoint, an #' unexpected disconnect waits for a replacement endpoint in that file and #' reconnects with the same runtime options and session identity. The optional -#' discovery jgdSocket string updates JGD_SOCKET when use_jgd is TRUE; an empty +#' discovery jgdSocket string updates JGD_SOCKET when the resolved backend +#' includes jgd; an empty #' string clears it, and an omitted field leaves it unchanged. Set #' `options(sess.quiet = TRUE)` to suppress the successful connection message. #' @export -connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE) { +connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, + use_jgd = FALSE, + plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) { + has_httpgd <- !missing(use_httpgd) + has_jgd <- !missing(use_jgd) + .warn_deprecated_plot_args(has_httpgd, has_jgd) + plot_backend <- if (missing(plot_backend) && (has_httpgd || has_jgd)) { + .legacy_plot_backend(use_httpgd, use_jgd) + } else { + .resolve_plot_backend(plot_backend) + } # Invalidate poll callbacks and restore a previous runtime before reconnecting. .transport_disconnect(silent = TRUE) .sess_env$con <- NULL @@ -39,11 +55,10 @@ connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, u # endpoints from the generated attach script. Never redirect an unrelated client. discovery <- if (nzchar(discovery_file)) .read_discovery(discovery_file) else NULL if (!is.null(discovery) && identical(discovery$endpoint, endpoint)) { - .configure_discovery_jgd(discovery, use_jgd) + .configure_discovery_jgd(discovery, plot_backend %in% c("auto", "jgd")) .sess_env$reconnect <- list( path = discovery_file, endpoint = endpoint, - options = list(use_rstudioapi = use_rstudioapi, - use_httpgd = use_httpgd, use_jgd = use_jgd) + options = list(use_rstudioapi = use_rstudioapi, plot_backend = plot_backend) ) } @@ -93,13 +108,9 @@ connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, u connected <- do_connect() if (is.na(use_rstudioapi)) use_rstudioapi <- TRUE - if (is.na(use_httpgd)) use_httpgd <- TRUE - if (is.na(use_jgd)) use_jgd <- FALSE if (isTRUE(connected) && !is.null(.sess_env$con)) { tryCatch( - runtime_start(use_rstudioapi = use_rstudioapi, - use_httpgd = use_httpgd, - use_jgd = use_jgd), + runtime_start(use_rstudioapi = use_rstudioapi, plot_backend = plot_backend), error = function(e) { phase <- .sess_env$runtime_start_phase error_call <- conditionCall(e) @@ -231,8 +242,8 @@ connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, u } # Discovery configures only the optional JGD renderer, never arbitrary R state. -.configure_discovery_jgd <- function(discovery, use_jgd) { - if (!isTRUE(use_jgd) || is.null(discovery$jgdSocket)) return(invisible(NULL)) +.configure_discovery_jgd <- function(discovery, jgd_enabled) { + if (!isTRUE(jgd_enabled) || is.null(discovery$jgdSocket)) return(invisible(NULL)) if (nzchar(discovery$jgdSocket)) { Sys.setenv(JGD_SOCKET = discovery$jgdSocket) } else { @@ -278,7 +289,8 @@ connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, u !identical(endpoint, settings$endpoint)) { # Read endpoint and renderer from one snapshot, even if the file is # replaced again while connect() runs. - .configure_discovery_jgd(discovery, settings$options$use_jgd) + .configure_discovery_jgd(discovery, + settings$options$plot_backend %in% c("auto", "jgd")) .sess_env$reconnecting <- TRUE tryCatch( do.call(connect, c(list(endpoint = endpoint), settings$options)), diff --git a/sess/README.md b/sess/README.md index 098d57871..f1ded1dff 100644 --- a/sess/README.md +++ b/sess/README.md @@ -46,11 +46,15 @@ When you start an R terminal from VS Code, the extension's R profile calls sess::connect( endpoint = NULL, # socket/pipe endpoint; see below use_rstudioapi = TRUE, # emulate rstudioapi functions - use_httpgd = TRUE, # allow httpgd as the plot device - use_jgd = FALSE # allow jgd as the plot device + plot_backend = "auto" # or "standard", "httpgd", "jgd", "native" ) ``` +If `plot_backend` is omitted, `sess::connect()` uses `auto`. The VS Code extension +passes its configured backend explicitly. Calls that supply the deprecated +`use_httpgd` or `use_jgd` arguments still use those values when `plot_backend` +is omitted. + If `endpoint` is omitted, `connect()` resolves it in this order: 1. The `SESS_ENDPOINT` environment variable. @@ -81,11 +85,11 @@ a new schema version. This discovery schema version is separate from the IPC `protocol_version`. The optional `jgdSocket` string describes the JGD renderer belonging to that -endpoint. When `use_jgd = TRUE`, `sess` applies it before runtime initialization, +endpoint. When `plot_backend` resolves to `auto` or `jgd`, `sess` applies it before runtime initialization, including automatic reconnect: a nonempty string sets `JGD_SOCKET`, an empty string unsets it (renderer unavailable), and an omitted field leaves it untouched. A present value of another type is invalid. It does not enable JGD or override -`use_jgd`; no arbitrary environment variables or R code are accepted. VS Code +the selected backend; no arbitrary environment variables or R code are accepted. VS Code publishes endpoint and renderer together in one atomic file replacement, with an empty `jgdSocket` when its current backend does not provide JGD. @@ -103,7 +107,7 @@ R's interactive features to the client: | `View()` | Data frames, matrices, Arrow tables and polars data frames open in a paged, sortable, filterable data viewer. Lists open as JSON; other objects as R code. | | `browseURL()`, `viewer`, `page_viewer` | URLs and local HTML files (e.g. htmlwidgets) open in the editor. | | `?topic`, `help.search()` | Help pages open in the editor's help panel, in the column configured by `r.session.viewers.viewColumn.helpPanel`. | -| Graphics device | Plots appear in the editor's plot viewer (see below). | +| Graphics device | Plots appear in the editor's plot viewer unless `plot_backend = "native"`. | | `rstudioapi` | Editor functions such as `getActiveDocumentContext()` and `insertText()` are emulated when `use_rstudioapi = TRUE`. | | Top-level task callback | The client is notified after each command so it can refresh the workspace view. | @@ -116,18 +120,24 @@ stacking hooks. ### Graphics devices -For displaying R plots, `sess` chooses a graphics device in this order: +For displaying R plots, `sess` chooses a graphics device in this order when +`plot_backend = "auto"`: -1. **jgd**, if `use_jgd = TRUE`, the `JGD_SOCKET` environment variable is set, - and the [jgd](https://cran.r-project.org/package=jgd) package is installed. -2. **httpgd**, if `use_httpgd = TRUE` and the - [httpgd](https://cran.r-project.org/package=httpgd) package is installed. +1. **httpgd**, if the [httpgd](https://cran.r-project.org/package=httpgd) + package is installed. +2. **jgd**, if `JGD_SOCKET` is set and the + [jgd](https://cran.r-project.org/package=jgd) package is installed. 3. **Standard**: plots are recorded on a null device and re-rendered by the client on demand at the viewer's size (as SVG via [svglite](https://cran.r-project.org/package=svglite) if installed, otherwise PNG). In VS Code, this is controlled by the `r.plot.backend` setting. +`plot_backend = "native"` leaves the existing R graphics device option, plot +hooks, plot task callbacks, and devices untouched. The `standard` backend continues +to use the static plot viewer. When `plot_backend` is omitted, the existing +`use_httpgd`/`use_jgd` arguments keep their previous meanings, but are +deprecated and warn when supplied explicitly. Use `plot_backend` for new code. ### Options and environment variables @@ -135,7 +145,7 @@ In VS Code, this is controlled by the `r.plot.backend` setting. |---|---|---| | `SESS_ENDPOINT` | env var | Socket/pipe path used by `connect()`. | | `SESS_RSTUDIOAPI` | env var | `TRUE`/`FALSE`; passed as `use_rstudioapi` by the extension's R profile. | -| `SESS_PLOT_BACKEND` | env var | `auto`, `standard`, `httpgd` or `jgd`; sets `use_httpgd`/`use_jgd` in the extension's R profile. | +| `SESS_PLOT_BACKEND` | env var | `auto`, `standard`, `httpgd`, `jgd` or `native`; passed as `plot_backend` by the extension's R profile. | | `JGD_SOCKET` | env var | Socket used by the jgd device; set by the extension. | | `sess.quiet` | R option | Set to `TRUE` to suppress the successful connection message. Connection failures remain visible. | diff --git a/sess/inst/tinytest/test-connect-failure.R b/sess/inst/tinytest/test-connect-failure.R index 73a651c50..e199212c7 100644 --- a/sess/inst/tinytest/test-connect-failure.R +++ b/sess/inst/tinytest/test-connect-failure.R @@ -63,7 +63,7 @@ local({ expect_identical(getTaskCallbackNames(), original_callbacks) expect_identical(getHook("plot.new"), original_plot_hook) } - opts <- list(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + opts <- list(use_rstudioapi = FALSE, plot_backend = "standard") listen() expect_error(do.call(env$connect, c(list(endpoint = endpoint), opts)), "injected runtime failure") diff --git a/sess/inst/tinytest/test-connection-message.R b/sess/inst/tinytest/test-connection-message.R index 7c960d287..64ea48db8 100644 --- a/sess/inst/tinytest/test-connection-message.R +++ b/sess/inst/tinytest/test-connection-message.R @@ -19,7 +19,7 @@ local({ assign(name, fun, env) } env$interactive <- function() TRUE - opts <- list(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + opts <- list(use_rstudioapi = FALSE, plot_backend = "standard") connect_once <- function(quiet) { path <- tempfile(fileext = ".sock") diff --git a/sess/inst/tinytest/test-ipc.R b/sess/inst/tinytest/test-ipc.R index c1a699594..ef862e299 100644 --- a/sess/inst/tinytest/test-ipc.R +++ b/sess/inst/tinytest/test-ipc.R @@ -248,7 +248,7 @@ local({ expect_equal(.sess_env$dataviews, list()) expect_length(ls(.sess_env$dataview_registry, all.names = TRUE), 0L) - sess:::runtime_start(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + sess:::runtime_start(use_rstudioapi = FALSE, plot_backend = "standard") expect_equal(.sess_env$dataviews, list()) expect_length(ls(.sess_env$dataview_registry, all.names = TRUE), 0L) expect_true(isTRUE(sess:::.runtime_state()$active)) @@ -276,7 +276,7 @@ local({ runtime_device <- grDevices::dev.cur() callbacks_after_first_start <- getTaskCallbackNames() - sess:::runtime_start(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + sess:::runtime_start(use_rstudioapi = FALSE, plot_backend = "standard") expect_equal(.sess_env$dataviews, list()) expect_length(ls(.sess_env$dataview_registry, all.names = TRUE), 0L) expect_equal(length(grep("^sess.workspace$", getTaskCallbackNames())), 1L) @@ -335,7 +335,7 @@ local({ .sess_env$latest_plot_path <- old_plot_path }, add = TRUE) - sess:::runtime_start(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + sess:::runtime_start(use_rstudioapi = FALSE, plot_backend = "standard") options(viewer = user_viewer) if (binding_was_locked) unlockBinding("View", utils_ns) assign("View", user_view, envir = utils_ns) @@ -366,7 +366,7 @@ local({ .sess_env$latest_plot_path <- old_plot_path }, add = TRUE) - sess:::runtime_start(use_rstudioapi = TRUE, use_httpgd = FALSE, use_jgd = FALSE) + sess:::runtime_start(use_rstudioapi = TRUE, plot_backend = "standard") expect_false(identical(get("isAvailable", rstudioapi_ns, inherits = FALSE), original_is_available)) expect_false(identical(getHook(rstudioapi_hook_name), original_load_hook)) @@ -424,7 +424,7 @@ local({ } else { .sess_env$transport_generation + 1L } - sess:::runtime_start(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + sess:::runtime_start(use_rstudioapi = FALSE, plot_backend = "standard") expect_true(isTRUE(sess:::.runtime_state()$active)) close(cons[[1L]]) @@ -601,7 +601,7 @@ local({ connected <- tryCatch({ sess::connect(endpoint = first$path, use_rstudioapi = FALSE, - use_httpgd = FALSE, use_jgd = FALSE) + plot_backend = "standard") first_peer <- accept_peer(first$server) !is.null(first_peer) && !is.null(.sess_env$con) }, error = function(e) FALSE) @@ -639,7 +639,7 @@ local({ expect_false(is.null(second_peer)) expect_equal(sess:::.session_id(), identity) expect_equal(.sess_env$reconnect$options, - list(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE)) + list(use_rstudioapi = FALSE, plot_backend = "standard")) expect_true(isTRUE(sess:::.runtime_state()$active)) expect_equal(length(grep("^sess.workspace$", getTaskCallbackNames())), 1L) sess:::.transport_disconnect() @@ -667,7 +667,7 @@ local({ }, add = TRUE) connected <- tryCatch({ - sess::connect(endpoint = path, use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + sess::connect(endpoint = path, use_rstudioapi = FALSE, plot_backend = "standard") ready <- processx::poll(list(server), 1000L) if (ready[[1]] %in% c("connect", "ready")) { processx::conn_accept_unix_socket(server) diff --git a/sess/inst/tinytest/test-native-plot.R b/sess/inst/tinytest/test-native-plot.R new file mode 100644 index 000000000..3416c056e --- /dev/null +++ b/sess/inst/tinytest/test-native-plot.R @@ -0,0 +1,148 @@ +# Explicit backends preserve the old boolean API and keep native distinct from +# the standard static viewer. +local({ + resolve <- sess:::.resolve_plot_backend + legacy <- sess:::.legacy_plot_backend + select <- sess:::.select_plot_backend + choices <- c("auto", "jgd", "httpgd", "standard", "native") + expect_equal(eval(formals(sess::connect)$plot_backend), choices) + expect_equal(eval(formals(sess::register_hooks)$plot_backend), choices) + expect_equal(resolve(choices), "auto") + expect_equal(resolve(NULL), "auto") + expect_equal(legacy(FALSE, FALSE), "standard") + expect_equal(legacy(TRUE, FALSE), "httpgd") + expect_equal(legacy(FALSE, TRUE), "jgd") + expect_equal(legacy(TRUE, TRUE), "auto") + for (backend in c("auto", "jgd", "httpgd", "standard", "native")) { + expect_equal(resolve(backend), backend) + } + expect_equal(select("auto", TRUE, TRUE), "httpgd") + expect_equal(select("auto", FALSE, TRUE), "jgd") + expect_equal(select("auto", FALSE, FALSE), "standard") + expect_equal(select("jgd", TRUE, TRUE), "jgd") + expect_equal(select("httpgd", FALSE, TRUE), "standard") + expect_equal(select("native", TRUE, TRUE), "native") +}) + +local({ + sess:::runtime_stop() + sentinel_device <- function(...) stop("R's device option was used") + original_device <- getOption("device") + original_null_dev <- getOption("sess.null_dev") + original_plot_hook <- getHook("plot.new") + original_grid_hook <- getHook("grid.newpage") + original_devices <- grDevices::dev.list() + options(device = sentinel_device) + on.exit({ + sess:::runtime_stop() + options(device = original_device) + }, add = TRUE) + + sess::register_hooks(use_rstudioapi = FALSE, plot_backend = "native") + state <- sess:::.runtime_state() + expect_true(isTRUE(state$active)) + expect_true(identical(getOption("device"), sentinel_device)) + expect_equal(getOption("sess.null_dev"), original_null_dev) + expect_equal(getHook("plot.new"), original_plot_hook) + expect_equal(getHook("grid.newpage"), original_grid_hook) + expect_equal(grDevices::dev.list(), original_devices) + expect_false("sess.plot" %in% unlist(state$task_callbacks)) + expect_true("sess.workspace" %in% unlist(state$task_callbacks)) + expect_length(state$devices, 0L) + expect_true(is.function(getOption("viewer"))) + + sess:::runtime_stop() + expect_true(identical(getOption("device"), sentinel_device)) + expect_equal(grDevices::dev.list(), original_devices) +}) + +# Legacy arguments still work, but public callers are directed to plot_backend. +local({ + old_device <- getOption("device") + sentinel_device <- function(...) stop("R's device option was used") + options(device = sentinel_device) + on.exit({ + sess:::runtime_stop() + options(device = old_device) + }, add = TRUE) + + warnings <- character() + withCallingHandlers( + sess::register_hooks(use_rstudioapi = FALSE, use_httpgd = TRUE, + use_jgd = TRUE, plot_backend = "native"), + warning = function(w) { + warnings <<- c(warnings, conditionMessage(w)) + invokeRestart("muffleWarning") + } + ) + expect_length(warnings, 1L) + expect_true(grepl("use_httpgd and use_jgd", warnings[[1L]], fixed = TRUE)) + expect_true(grepl("plot_backend", warnings[[1L]], fixed = TRUE)) + expect_true(identical(getOption("device"), sentinel_device)) +}) + +local({ + env <- new.env(parent = asNamespace("sess")) + env$connect <- sess::connect + environment(env$connect) <- env + env$.resolve_endpoint <- function(...) "" + warnings <- character() + invisible(capture.output(withCallingHandlers( + env$connect(use_httpgd = FALSE), + warning = function(w) { + warnings <<- c(warnings, conditionMessage(w)) + invokeRestart("muffleWarning") + } + ))) + expect_true(any(grepl("use_httpgd is deprecated", warnings, fixed = TRUE))) +}) + +# The public connection path stores native for discovery reconnects and still +# initializes the non-plot runtime without replacing a profile device option. +local({ + endpoint <- if (.Platform$OS.type == "windows") { + paste0("\\\\?\\pipe\\", basename(tempfile("sess-native-"))) + } else { + tempfile(fileext = ".sock") + } + server <- tryCatch(processx::conn_create_unix_socket(endpoint, encoding = ""), + error = function(e) NULL) + if (is.null(server)) return(invisible(NULL)) + peer <- NULL + discovery <- tempfile(fileext = ".json") + writeLines(jsonlite::toJSON(list(version = 1L, endpoint = endpoint), + auto_unbox = TRUE), discovery) + old_discovery <- Sys.getenv("SESS_DISCOVERY_FILE", unset = NA_character_) + Sys.setenv(SESS_DISCOVERY_FILE = discovery) + sentinel_device <- function(...) stop("R's device option was used") + original_device <- getOption("device") + original_devices <- grDevices::dev.list() + options(device = sentinel_device) + on.exit({ + sess:::.transport_disconnect(silent = TRUE) + if (!is.null(peer)) try(close(peer), silent = TRUE) + try(close(server), silent = TRUE) + unlink(c(discovery, if (.Platform$OS.type != "windows") endpoint)) + if (is.na(old_discovery)) Sys.unsetenv("SESS_DISCOVERY_FILE") else + Sys.setenv(SESS_DISCOVERY_FILE = old_discovery) + options(device = original_device) + }, add = TRUE) + + env <- new.env(parent = asNamespace("sess")) + env$connect <- sess::connect + environment(env$connect) <- env + env$poll_connection <- function(...) { + processx::poll(list(server), 1000L) + peer <<- processx::conn_accept_unix_socket(server) + processx::poll(list(peer), 1000L) + expect_equal(jsonlite::fromJSON(processx::conn_read_chars(peer))$method, + "attach") + } + env$connect(endpoint = endpoint, use_rstudioapi = FALSE, + plot_backend = "native") + expect_true(isTRUE(sess:::.runtime_state()$active)) + expect_true(identical(getOption("device"), sentinel_device)) + expect_equal(grDevices::dev.list(), original_devices) + expect_equal(sess:::.sess_env$reconnect$options$plot_backend, "native") + expect_false("sess.plot" %in% getTaskCallbackNames()) +}) diff --git a/sess/inst/tinytest/test-plot-notification.R b/sess/inst/tinytest/test-plot-notification.R index 3c01e13a6..c2fb3cb0f 100644 --- a/sess/inst/tinytest/test-plot-notification.R +++ b/sess/inst/tinytest/test-plot-notification.R @@ -38,7 +38,7 @@ local({ env$.defer_runtime_notification <- function(method) { sess:::.defer_runtime_notification(method, schedule = scheduler) } - env$runtime_start(use_rstudioapi = FALSE, use_httpgd = FALSE, use_jgd = FALSE) + env$runtime_start(use_rstudioapi = FALSE, plot_backend = "standard") # tinytest already has a PDF device open. Explicitly open the sess device # instead of relying on plot() to use the default device factory. getOption("device")() diff --git a/sess/inst/tinytest/test-reconnect.R b/sess/inst/tinytest/test-reconnect.R index b80fe7a64..23c6065ec 100644 --- a/sess/inst/tinytest/test-reconnect.R +++ b/sess/inst/tinytest/test-reconnect.R @@ -36,7 +36,7 @@ local({ path <- tempfile() on.exit(unlink(path), add = TRUE) settings <- list(path = path, endpoint = "old", options = list( - use_rstudioapi = FALSE, use_httpgd = TRUE, use_jgd = FALSE + use_rstudioapi = FALSE, plot_backend = "httpgd" )) env$.schedule_reconnect(settings, schedule = schedule) tick() # Missing discovery file. @@ -73,7 +73,7 @@ local({ expect_equal(length(callbacks), 0L) # Renderer metadata must be installed before connect starts the runtime. - settings$options$use_jgd <- TRUE + settings$options$plot_backend <- "jgd" for (socket in c("replacement-jgd", "")) { env$.sess_env$con <- NULL Sys.setenv(JGD_SOCKET = "dead-jgd") @@ -111,4 +111,17 @@ local({ expect_equal(length(attempts), count) expect_equal(length(callbacks), 0L) + # An explicit native backend must survive the same reconnect path. + native_options <- list(use_rstudioapi = FALSE, plot_backend = "native") + native_settings <- list(path = path, endpoint = "old", options = native_options) + env$.sess_env$con <- NULL + Sys.setenv(JGD_SOCKET = "external-jgd") + writeLines('{"version":1,"endpoint":"new","jgdSocket":"unused"}', path) + env$.schedule_reconnect(native_settings, schedule = schedule) + tick() + expect_equal(tail(attempts, 1L)[[1L]], + c(list(endpoint = "new"), native_options)) + expect_equal(Sys.getenv("JGD_SOCKET"), "external-jgd") + expect_equal(length(callbacks), 0L) + }) diff --git a/sess/man/connect.Rd b/sess/man/connect.Rd index b740c0fdb..f9449977e 100644 --- a/sess/man/connect.Rd +++ b/sess/man/connect.Rd @@ -8,7 +8,8 @@ connect( endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, - use_jgd = FALSE + use_jgd = FALSE, + plot_backend = c("auto", "jgd", "httpgd", "standard", "native") ) } \arguments{ @@ -17,9 +18,15 @@ If NULL, uses SESS_ENDPOINT, then SESS_DISCOVERY_FILE.} \item{use_rstudioapi}{Logical. Enable rstudioapi emulation. Defaults to TRUE.} -\item{use_httpgd}{Logical. Use httpgd for plotting if available. Defaults to TRUE.} +\item{use_httpgd}{Deprecated. Logical. Use httpgd for plotting if available. +Defaults to TRUE. Use \code{plot_backend} instead.} -\item{use_jgd}{Logical. Use jgd for plotting if available. Defaults to FALSE.} +\item{use_jgd}{Deprecated. Logical. Use jgd for plotting if available. +Defaults to FALSE. Use \code{plot_backend} instead.} + +\item{plot_backend}{Plot backend: \code{auto}, \code{jgd}, \code{httpgd}, +\code{standard}, or \code{native}. NULL also selects \code{auto}. Deprecated +flags select the backend only when this argument is omitted.} } \description{ Connect to the VS Code IPC server @@ -28,7 +35,8 @@ Connect to the VS Code IPC server When SESS_DISCOVERY_FILE describes the connected endpoint, an unexpected disconnect waits for a replacement endpoint in that file and reconnects with the same runtime options and session identity. The optional -discovery jgdSocket string updates JGD_SOCKET when use_jgd is TRUE; an empty +discovery jgdSocket string updates JGD_SOCKET when the resolved backend +includes jgd; an empty string clears it, and an omitted field leaves it unchanged. Set \code{options(sess.quiet = TRUE)} to suppress the successful connection message. } diff --git a/sess/man/register_hooks.Rd b/sess/man/register_hooks.Rd index 8fd0882b9..c141d3138 100644 --- a/sess/man/register_hooks.Rd +++ b/sess/man/register_hooks.Rd @@ -4,14 +4,21 @@ \alias{register_hooks} \title{Register VS Code runtime integrations} \usage{ -register_hooks(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE) +register_hooks(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE, + plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) } \arguments{ \item{use_rstudioapi}{Logical. Enable rstudioapi emulation.} -\item{use_httpgd}{Logical. Enable httpgd plot device if available.} +\item{use_httpgd}{Deprecated. Logical. Enable httpgd plot device if available. +Use \code{plot_backend} instead.} -\item{use_jgd}{Logical. Enable jgd plot device if available.} +\item{use_jgd}{Deprecated. Logical. Enable jgd plot device if available. +Use \code{plot_backend} instead.} + +\item{plot_backend}{Plot backend: \code{auto}, \code{jgd}, \code{httpgd}, +\code{standard}, or \code{native}. NULL also selects \code{auto}. Deprecated +flags select the backend only when this argument is omitted.} } \description{ Register VS Code runtime integrations diff --git a/sess/man/runtime_start.Rd b/sess/man/runtime_start.Rd index 37c1172a1..e36c86371 100644 --- a/sess/man/runtime_start.Rd +++ b/sess/man/runtime_start.Rd @@ -4,7 +4,8 @@ \alias{runtime_start} \title{Start the VS Code runtime integration (internal)} \usage{ -runtime_start(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE) +runtime_start(use_rstudioapi = TRUE, + plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) } \description{ Start the VS Code runtime integration (internal) diff --git a/src/api.d.ts b/src/api.d.ts index 9000f806b..15af078c1 100644 --- a/src/api.d.ts +++ b/src/api.d.ts @@ -15,7 +15,7 @@ export interface RSessionConnectionInfo { protocolVersion: number; endpoint: string; /** The configured, compatibility-resolved plot backend preference. */ - plotBackend: 'auto' | 'standard' | 'httpgd' | 'jgd'; + plotBackend: 'auto' | 'standard' | 'httpgd' | 'jgd' | 'native'; /** Present when plotBackend is jgd or auto and the JGD socket is available. */ jgdSocket?: string; } diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts index 964db5f78..c5b332cc3 100644 --- a/src/plotViewer/index.ts +++ b/src/plotViewer/index.ts @@ -9,7 +9,9 @@ import { extensionContext } from '../extension'; import { config } from '../util'; import { getMigratedSetting } from '../configuration'; -export function resolveBackend(): 'auto' | 'standard' | 'httpgd' | 'jgd' { +export type PlotBackend = 'auto' | 'standard' | 'httpgd' | 'jgd' | 'native'; + +export function resolveBackend(): PlotBackend { const selected = getMigratedSetting( config(), 'plot.backend', @@ -19,7 +21,7 @@ export function resolveBackend(): 'auto' | 'standard' | 'httpgd' | 'jgd' { if (selected === true) { return 'httpgd'; } - return typeof selected === 'string' ? selected as 'standard' | 'httpgd' | 'jgd' : 'auto'; + return typeof selected === 'string' ? selected as PlotBackend : 'auto'; } export function jgdEnabled(backend = resolveBackend()): boolean { @@ -66,9 +68,13 @@ export class CommonPlotManager implements PlotManager { } get activeViewer(): PlotViewer | undefined { - if (jgdEnabled()) { + const backend = resolveBackend(); + if (backend === 'jgd') { return this.jgdManager.getViewer() || this.httpgdManager.getRecentViewer() || this.standardPlotViewer; } + if (backend === 'auto') { + return this.httpgdManager.getRecentViewer() || this.jgdManager.getViewer() || this.standardPlotViewer; + } return this.httpgdManager.getRecentViewer() || this.standardPlotViewer; } diff --git a/src/session.ts b/src/session.ts index 9d094dd69..de0ceac7c 100644 --- a/src/session.ts +++ b/src/session.ts @@ -662,16 +662,15 @@ function getAttachSessionScriptPath(pipePath: string): string { function buildAttachSessionScript(pipePath: string, sessPath: string, installSessScriptPath: string): string { const backend = resolveBackend(); - const useHttpgd = backend === 'httpgd' || backend === 'auto' ? 'TRUE' : 'FALSE'; - const jgd = jgdEnabled(backend); - const useJgd = jgd ? 'TRUE' : 'FALSE'; const jgdSocket = getSessionJgdSocket(); return [ 'local({', ` endpoint <- ${asRStringLiteral(pipePath)}`, ` sess_src <- ${asRStringLiteral(sessPath)}`, ` install_sess_script <- ${asRStringLiteral(installSessScriptPath)}`, - jgdSocket ? ` Sys.setenv(JGD_SOCKET = ${asRStringLiteral(jgdSocket)})` : ' Sys.unsetenv("JGD_SOCKET")', + ...(backend === 'native' ? [] : [ + jgdSocket ? ` Sys.setenv(JGD_SOCKET = ${asRStringLiteral(jgdSocket)})` : ' Sys.unsetenv("JGD_SOCKET")', + ]), ' bundled_version <- tryCatch(read.dcf(file.path(sess_src, "DESCRIPTION"))[1, "Version"], error = function(e) NA_character_)', ' installed_version <- suppressWarnings(tryCatch(as.character(utils::packageVersion("sess")), error = function(e) NA_character_))', ' needs_install <- is.na(installed_version) || (!is.na(bundled_version) && utils::compareVersion(installed_version, bundled_version) < 0)', @@ -683,7 +682,7 @@ function buildAttachSessionScript(pipePath: string, sessPath: string, installSes ' on.exit(Sys.unsetenv(c("VSCODE_R_SESS_PKG_PATH", "VSCODE_R_SESS_REPO")), add = TRUE)', ' source(install_sess_script, local = TRUE)', ' }', - ` sess::connect(endpoint = endpoint, use_httpgd = ${useHttpgd}, use_jgd = ${useJgd})`, + ` sess::connect(endpoint = endpoint, plot_backend = ${asRStringLiteral(backend)})`, '})', '', ].join('\n'); diff --git a/src/test/suite/plotBackend.test.ts b/src/test/suite/plotBackend.test.ts index e9d2763af..15ba56cd3 100644 --- a/src/test/suite/plotBackend.test.ts +++ b/src/test/suite/plotBackend.test.ts @@ -2,7 +2,9 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; import * as util from '../../util'; -import { resolveBackend } from '../../plotViewer'; +import { resolveBackend, jgdEnabled, CommonPlotManager } from '../../plotViewer'; +import { HttpgdViewer } from '../../plotViewer/httpgdViewer'; +import { JgdViewer } from '../../plotViewer/jgdViewer'; suite('Plot backend setting migration', () => { let sandbox: sinon.SinonSandbox; @@ -44,4 +46,20 @@ suite('Plot backend setting migration', () => { settings(); assert.strictEqual(resolveBackend(), 'auto'); }); + + test('native overrides the legacy httpgd preference and disables JGD', () => { + settings({ workspaceValue: 'native' }, { globalValue: true }); + assert.strictEqual(resolveBackend(), 'native'); + assert.strictEqual(jgdEnabled(resolveBackend()), false); + }); + + test('auto prefers an available httpgd viewer over JGD', () => { + settings({ workspaceValue: 'auto' }); + const manager = new CommonPlotManager(); + const httpgd = { id: 'httpgd' } as HttpgdViewer; + const jgd = { id: 'jgd' } as JgdViewer; + sandbox.stub(manager.httpgdManager, 'getRecentViewer').returns(httpgd); + sandbox.stub(manager.jgdManager, 'getViewer').returns(jgd); + assert.strictEqual(manager.activeViewer, httpgd); + }); }); diff --git a/src/test/suite/sessInstall.test.ts b/src/test/suite/sessInstall.test.ts index 177a908ae..358485d13 100644 --- a/src/test/suite/sessInstall.test.ts +++ b/src/test/suite/sessInstall.test.ts @@ -79,7 +79,7 @@ suite('Sess Install Test Suite', () => { assert.strictEqual(showMessageStub.called, false); }); - test('sess package version matches extension version', () => { + test('bundled sess package is at least as new as the extension', () => { const packageJsonPath = path.join(extension_root, 'package.json'); const descriptionPath = path.join(extension_root, 'sess', 'DESCRIPTION'); @@ -94,6 +94,7 @@ suite('Sess Install Test Suite', () => { const sessVersion = match ? match[1] : undefined; const baseVersion = packageJson.version.split('-')[0]; - assert.strictEqual(sessVersion, baseVersion, 'sess package version should match base extension version in package.json'); + assert.ok(sessVersion && util.compareVersions(sessVersion, baseVersion) >= 0, + 'bundled sess version must not be older than the extension version'); }); }); diff --git a/src/test/suite/session.test.ts b/src/test/suite/session.test.ts index a8eb61aba..774c36085 100644 --- a/src/test/suite/session.test.ts +++ b/src/test/suite/session.test.ts @@ -234,8 +234,9 @@ suite('Session Communication', () => { assert.strictEqual(await api.getConnectionInfo(), undefined); watcher.value(true); + let backend = 'standard'; const configStub = { - get: (key: string) => key === 'plot.backend' ? 'standard' : undefined, + get: (key: string) => key === 'plot.backend' ? backend : undefined, }; sandbox.stub(util, 'config').returns(configStub as unknown as vscode.WorkspaceConfiguration); await waitFor(() => session.globalPipePath); @@ -246,6 +247,11 @@ suite('Session Communication', () => { assert.strictEqual(connection.endpoint, session.globalPipePath); assert.strictEqual(connection.plotBackend, 'standard'); assert.ok(!('socket' in connection), 'connection info should contain plain contract data only'); + + backend = 'native'; + const nativeConnection = await api.getConnectionInfo(); + assert.strictEqual(nativeConnection?.plotBackend, 'native'); + assert.strictEqual(nativeConnection?.jgdSocket, undefined); }); test('public session API activates a connected session by id and rejects missing or disconnected sessions', async () => { @@ -627,6 +633,19 @@ suite('Session Communication', () => { } }); + test('attach script passes the native backend to sess', async () => { + sandbox.stub(util, 'config').returns({ + get: (key: string) => key === 'plot.backend' ? 'native' : undefined, + } as unknown as vscode.WorkspaceConfiguration); + const command = await session.getAttachSessionCommand(); + const commandMatch = command.match(/^source\((.*)\)$/); + assert.ok(commandMatch); + const scriptPath = JSON.parse(commandMatch[1]) as string; + const scriptContent = await fs.readFile(scriptPath, 'utf8'); + assert.match(scriptContent, /sess::connect\(endpoint = endpoint, plot_backend = "native"\)/); + assert.doesNotMatch(scriptContent, /Sys\.(?:setenv|unsetenv)\(JGD_SOCKET/); + }); + test('manual recovery targets the selected managed terminal while another session is active', async () => { const endpoint = await session.getGlobalPipePath(); const first = { diff --git a/src/test/suite/terminal.test.ts b/src/test/suite/terminal.test.ts index a38422439..772f6ade0 100644 --- a/src/test/suite/terminal.test.ts +++ b/src/test/suite/terminal.test.ts @@ -222,6 +222,25 @@ suite('R Terminal', () => { assert.strictEqual(options.env['SESS_PLOT_BACKEND'], 'standard'); }); + test('makeTerminalOptions passes native to sess without a JGD socket', async () => { + sandbox.stub(util, 'config').returns(configuration({ + sessionWatcher: true, + 'plot.backend': 'native', + 'plot.useHttpgd': true, + })); + sandbox.stub(util, 'getRterm').resolves(process.execPath); + const options = await rTerminal.makeTerminalOptions(); + const discoveryFile = options.env?.['SESS_DISCOVERY_FILE']; + try { + assert.strictEqual(options.env?.['SESS_PLOT_BACKEND'], 'native'); + assert.strictEqual(options.env?.['JGD_SOCKET'], undefined); + } finally { + if (typeof discoveryFile === 'string') { + await fs.remove(discoveryFile); + } + } + }); + test('makeTerminalOptions does not set session watcher env if disabled', async () => { const configStub = { get: (key: string) => { From c55bc823d35dfdd3bf04b2e490126dcf71beffd6 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 06:57:52 +0000 Subject: [PATCH 2/8] fix: preserve JGD auto priority and repair native test --- README.md | 2 +- package.json | 4 ++-- sess/R/hooks.R | 8 ++++---- sess/README.md | 6 +++--- sess/inst/tinytest/test-native-plot.R | 11 +++++------ src/plotViewer/index.ts | 6 +----- src/test/suite/plotBackend.test.ts | 4 ++-- 7 files changed, 18 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 0d61a0dae..632d1585d 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ For example, assuming that you have installed `arf` and `jgd`, your user `settin { "r.consolePath": "arf", "r.bracketedPaste": true, // recommended for arf (and radian) - "r.plot.backend": "jgd" // "auto" prefers httpgd, then jgd + "r.plot.backend": "jgd" // "auto" already prefers jgd, then httpgd } ``` diff --git a/package.json b/package.json index 488444c8e..c82ff5244 100644 --- a/package.json +++ b/package.json @@ -1875,13 +1875,13 @@ "native" ], "markdownEnumDescriptions": [ - "Automatic: tries httpgd first (if installed), then JGD, then standard. Existing `#r.plot.useHttpgd#` configurations remain supported.", + "Automatic: tries JGD first (if installed), then httpgd, then standard. Existing `#r.plot.useHttpgd#` configurations remain supported.", "Standard static plot viewer (PNG/SVG)", "httpgd-based interactive plot viewer (requires `httpgd` R package)", "JGD-based interactive plot viewer (requires `jgd` R package)", "Use R's configured graphics device without plot integration or an automatic VS Code plot viewer" ], - "markdownDescription": "The canonical setting for selecting the plot backend. `standard` uses the VS Code static plot viewer; `native` leaves R's configured graphics device untouched.\n\nWhen set to `auto`, the best available backend is used (httpgd if installed, then JGD, then standard). For backwards compatibility, setting `#r.plot.useHttpgd#` to `true` selects httpgd while this setting remains `auto`; workspace settings take precedence over user settings, with an explicit backend value preferred within the same scope." + "markdownDescription": "The canonical setting for selecting the plot backend. `standard` uses the VS Code static plot viewer; `native` leaves R's configured graphics device untouched.\n\nWhen set to `auto`, the best available backend is used (JGD if installed, then httpgd, then standard). For backwards compatibility, setting `#r.plot.useHttpgd#` to `true` selects httpgd while this setting remains `auto`; workspace settings take precedence over user settings, with an explicit backend value preferred within the same scope." }, "r.plot.jgd.historyLimit": { "type": "number", diff --git a/sess/R/hooks.R b/sess/R/hooks.R index 749d616d0..2c07e816a 100644 --- a/sess/R/hooks.R +++ b/sess/R/hooks.R @@ -46,8 +46,8 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F .select_plot_backend <- function(plot_backend, has_httpgd, has_jgd) { if (plot_backend == "native") return("native") - if (plot_backend %in% c("auto", "httpgd") && has_httpgd) return("httpgd") if (plot_backend %in% c("auto", "jgd") && has_jgd) return("jgd") + if (plot_backend %in% c("auto", "httpgd") && has_httpgd) return("httpgd") "standard" } @@ -213,12 +213,12 @@ runtime_start <- function(use_rstudioapi = TRUE, } invisible(x) } - # 4. Plot device: httpgd > JGD > Standard, or no plot integration for native + # 4. Plot device: JGD > httpgd > Standard, or no plot integration for native .sess_env$runtime_start_phase <- "plot" + has_jgd <- plot_backend %in% c("auto", "jgd") && + nzchar(Sys.getenv("JGD_SOCKET")) && requireNamespace("jgd", quietly = TRUE) has_httpgd <- plot_backend %in% c("auto", "httpgd") && requireNamespace("httpgd", quietly = TRUE) - has_jgd <- (plot_backend == "jgd" || (plot_backend == "auto" && !has_httpgd)) && - nzchar(Sys.getenv("JGD_SOCKET")) && requireNamespace("jgd", quietly = TRUE) selected_backend <- .select_plot_backend(plot_backend, has_httpgd, has_jgd) if (selected_backend == "jgd") { .runtime_set_option("device", function(...) { diff --git a/sess/README.md b/sess/README.md index f1ded1dff..5301dc2d7 100644 --- a/sess/README.md +++ b/sess/README.md @@ -123,10 +123,10 @@ stacking hooks. For displaying R plots, `sess` chooses a graphics device in this order when `plot_backend = "auto"`: -1. **httpgd**, if the [httpgd](https://cran.r-project.org/package=httpgd) - package is installed. -2. **jgd**, if `JGD_SOCKET` is set and the +1. **jgd**, if `JGD_SOCKET` is set and the [jgd](https://cran.r-project.org/package=jgd) package is installed. +2. **httpgd**, if the [httpgd](https://cran.r-project.org/package=httpgd) + package is installed. 3. **Standard**: plots are recorded on a null device and re-rendered by the client on demand at the viewer's size (as SVG via [svglite](https://cran.r-project.org/package=svglite) if installed, diff --git a/sess/inst/tinytest/test-native-plot.R b/sess/inst/tinytest/test-native-plot.R index 3416c056e..a876291d9 100644 --- a/sess/inst/tinytest/test-native-plot.R +++ b/sess/inst/tinytest/test-native-plot.R @@ -16,7 +16,8 @@ local({ for (backend in c("auto", "jgd", "httpgd", "standard", "native")) { expect_equal(resolve(backend), backend) } - expect_equal(select("auto", TRUE, TRUE), "httpgd") + expect_equal(select("auto", TRUE, TRUE), "jgd") + expect_equal(select("auto", TRUE, FALSE), "httpgd") expect_equal(select("auto", FALSE, TRUE), "jgd") expect_equal(select("auto", FALSE, FALSE), "standard") expect_equal(select("jgd", TRUE, TRUE), "jgd") @@ -108,7 +109,6 @@ local({ server <- tryCatch(processx::conn_create_unix_socket(endpoint, encoding = ""), error = function(e) NULL) if (is.null(server)) return(invisible(NULL)) - peer <- NULL discovery <- tempfile(fileext = ".json") writeLines(jsonlite::toJSON(list(version = 1L, endpoint = endpoint), auto_unbox = TRUE), discovery) @@ -120,7 +120,6 @@ local({ options(device = sentinel_device) on.exit({ sess:::.transport_disconnect(silent = TRUE) - if (!is.null(peer)) try(close(peer), silent = TRUE) try(close(server), silent = TRUE) unlink(c(discovery, if (.Platform$OS.type != "windows") endpoint)) if (is.na(old_discovery)) Sys.unsetenv("SESS_DISCOVERY_FILE") else @@ -133,9 +132,9 @@ local({ environment(env$connect) <- env env$poll_connection <- function(...) { processx::poll(list(server), 1000L) - peer <<- processx::conn_accept_unix_socket(server) - processx::poll(list(peer), 1000L) - expect_equal(jsonlite::fromJSON(processx::conn_read_chars(peer))$method, + processx::conn_accept_unix_socket(server) + processx::poll(list(server), 1000L) + expect_equal(jsonlite::fromJSON(processx::conn_read_chars(server))$method, "attach") } env$connect(endpoint = endpoint, use_rstudioapi = FALSE, diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts index c5b332cc3..0ce1231b1 100644 --- a/src/plotViewer/index.ts +++ b/src/plotViewer/index.ts @@ -68,13 +68,9 @@ export class CommonPlotManager implements PlotManager { } get activeViewer(): PlotViewer | undefined { - const backend = resolveBackend(); - if (backend === 'jgd') { + if (jgdEnabled()) { return this.jgdManager.getViewer() || this.httpgdManager.getRecentViewer() || this.standardPlotViewer; } - if (backend === 'auto') { - return this.httpgdManager.getRecentViewer() || this.jgdManager.getViewer() || this.standardPlotViewer; - } return this.httpgdManager.getRecentViewer() || this.standardPlotViewer; } diff --git a/src/test/suite/plotBackend.test.ts b/src/test/suite/plotBackend.test.ts index 15ba56cd3..1912e7fe0 100644 --- a/src/test/suite/plotBackend.test.ts +++ b/src/test/suite/plotBackend.test.ts @@ -53,13 +53,13 @@ suite('Plot backend setting migration', () => { assert.strictEqual(jgdEnabled(resolveBackend()), false); }); - test('auto prefers an available httpgd viewer over JGD', () => { + test('auto prefers an available JGD viewer over httpgd', () => { settings({ workspaceValue: 'auto' }); const manager = new CommonPlotManager(); const httpgd = { id: 'httpgd' } as HttpgdViewer; const jgd = { id: 'jgd' } as JgdViewer; sandbox.stub(manager.httpgdManager, 'getRecentViewer').returns(httpgd); sandbox.stub(manager.jgdManager, 'getViewer').returns(jgd); - assert.strictEqual(manager.activeViewer, httpgd); + assert.strictEqual(manager.activeViewer, jgd); }); }); From ce6608da32cfbebf9392425d25de3996c0a0ba71 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 07:01:49 +0000 Subject: [PATCH 3/8] test: provide extension context for plot manager --- src/test/suite/plotBackend.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/suite/plotBackend.test.ts b/src/test/suite/plotBackend.test.ts index 1912e7fe0..8646df50e 100644 --- a/src/test/suite/plotBackend.test.ts +++ b/src/test/suite/plotBackend.test.ts @@ -1,10 +1,12 @@ import * as assert from 'assert'; +import * as path from 'path'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; import * as util from '../../util'; import { resolveBackend, jgdEnabled, CommonPlotManager } from '../../plotViewer'; import { HttpgdViewer } from '../../plotViewer/httpgdViewer'; import { JgdViewer } from '../../plotViewer/jgdViewer'; +import { mockExtensionContext } from '../common/mockvscode'; suite('Plot backend setting migration', () => { let sandbox: sinon.SinonSandbox; @@ -55,6 +57,7 @@ suite('Plot backend setting migration', () => { test('auto prefers an available JGD viewer over httpgd', () => { settings({ workspaceValue: 'auto' }); + mockExtensionContext(path.resolve(__dirname, '../../..'), sandbox); const manager = new CommonPlotManager(); const httpgd = { id: 'httpgd' } as HttpgdViewer; const jgd = { id: 'jgd' } as JgdViewer; From dc51e03725a9b32a821de0f0f4b4363dcefd7f50 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 07:09:52 +0000 Subject: [PATCH 4/8] fix: clear JGD terminal mutation outside JGD backends --- src/plotViewer/index.ts | 5 ++-- src/test/common/mockvscode.ts | 2 +- src/test/suite/plotBackend.test.ts | 43 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts index 0ce1231b1..c6b6b7698 100644 --- a/src/plotViewer/index.ts +++ b/src/plotViewer/index.ts @@ -101,13 +101,14 @@ export class CommonPlotManager implements PlotManager { private applyBackend(): void { const backend = resolveBackend(); void vscode.commands.executeCommand('setContext', 'r.plot.backend', backend); + const envCollection = extensionContext.environmentVariableCollection; + envCollection.persistent = false; if (!jgdEnabled(backend)) { + envCollection.delete('JGD_SOCKET'); return; } this.jgdManager.start(); // Set JGD_SOCKET env var for R child processes - const envCollection = extensionContext.environmentVariableCollection; - envCollection.persistent = false; for (const [key, value] of Object.entries(this.getJgdEnvVars())) { envCollection.replace(key, value); } diff --git a/src/test/common/mockvscode.ts b/src/test/common/mockvscode.ts index d72d31286..02ccde051 100644 --- a/src/test/common/mockvscode.ts +++ b/src/test/common/mockvscode.ts @@ -12,7 +12,7 @@ export function mockActiveTextEditor(document: vscode.TextDocument, sandbox: sin export function mockExtensionContext(extension_root: string, sandbox: sinon.SinonSandbox) { const mockExtensionContext = { - environmentVariableCollection: { persistent: false, replace: sandbox.stub() }, + environmentVariableCollection: { persistent: false, replace: sandbox.stub(), delete: sandbox.stub() }, extension: sandbox.stub(), extensionMode: sandbox.stub(), extensionPath: extension_root, diff --git a/src/test/suite/plotBackend.test.ts b/src/test/suite/plotBackend.test.ts index 8646df50e..fdb44c082 100644 --- a/src/test/suite/plotBackend.test.ts +++ b/src/test/suite/plotBackend.test.ts @@ -2,6 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; +import * as extension from '../../extension'; import * as util from '../../util'; import { resolveBackend, jgdEnabled, CommonPlotManager } from '../../plotViewer'; import { HttpgdViewer } from '../../plotViewer/httpgdViewer'; @@ -65,4 +66,46 @@ suite('Plot backend setting migration', () => { sandbox.stub(manager.jgdManager, 'getViewer').returns(jgd); assert.strictEqual(manager.activeViewer, jgd); }); + + test('backend changes replace or remove only the JGD socket mutation', () => { + const canonical = { workspaceValue: 'jgd' }; + settings(canonical); + mockExtensionContext(path.resolve(__dirname, '../../..'), sandbox); + sandbox.stub(vscode.commands, 'registerCommand'); + sandbox.stub(vscode.commands, 'executeCommand').resolves(); + const onChange = sandbox.stub(vscode.workspace, 'onDidChangeConfiguration'); + const manager = new CommonPlotManager(); + sandbox.stub(manager.jgdManager, 'initialize'); + const start = sandbox.stub(manager.jgdManager, 'start'); + sandbox.stub(manager, 'getJgdEnvVars').returns({ JGD_SOCKET: 'test-jgd-socket' }); + const collection = extension.extensionContext.environmentVariableCollection as unknown as { + replace: sinon.SinonStub; + delete: sinon.SinonStub; + }; + const replace = collection.replace; + const remove = collection.delete; + + manager.initialize(); + sinon.assert.calledOnceWithExactly(replace, 'JGD_SOCKET', 'test-jgd-socket'); + sinon.assert.notCalled(remove); + sinon.assert.calledOnce(start); + + const listener = onChange.firstCall.args[0] as (event: vscode.ConfigurationChangeEvent) => void; + for (const backend of ['auto', 'native', 'standard', 'httpgd'] as const) { + canonical.workspaceValue = backend; + replace.resetHistory(); + remove.resetHistory(); + start.resetHistory(); + listener({ affectsConfiguration: key => key === 'r.plot.backend' } as vscode.ConfigurationChangeEvent); + if (backend === 'auto') { + sinon.assert.calledOnceWithExactly(replace, 'JGD_SOCKET', 'test-jgd-socket'); + sinon.assert.notCalled(remove); + sinon.assert.calledOnce(start); + } else { + sinon.assert.notCalled(replace); + sinon.assert.calledOnceWithExactly(remove, 'JGD_SOCKET'); + sinon.assert.notCalled(start); + } + } + }); }); From 2c63f251e9536b9048f04384b2d720ebd3b96c9c Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 07:17:30 +0000 Subject: [PATCH 5/8] fix: keep plot backend settings machine specific --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index c82ff5244..f7ba7538f 100644 --- a/package.json +++ b/package.json @@ -1860,6 +1860,7 @@ "r.plot.useHttpgd": { "type": "boolean", "default": false, + "scope": "machine-overridable", "markdownDescription": "Legacy compatibility setting for selecting the httpgd plot viewer. When `#r.plot.backend#` is `auto`, setting this to `true` selects the `httpgd` backend. Workspace settings take precedence over user settings. Within the same scope, an explicit `#r.plot.backend#` value takes precedence.\n\nRequires the `httpgd` R package version 1.2.0 or later.", "markdownDeprecationMessage": "Deprecated: use `#r.plot.backend#` instead. This setting is retained for compatibility with existing configurations but will be removed in a future release.", "deprecationMessage": "Deprecated: use r.plot.backend instead. This setting is retained for compatibility with existing configurations but will be removed in a future release." @@ -1867,6 +1868,7 @@ "r.plot.backend": { "type": "string", "default": "auto", + "scope": "machine-overridable", "enum": [ "auto", "standard", From 254c2bb49d99d8d7608923f06c017dd8336a199d Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 08:26:32 +0000 Subject: [PATCH 6/8] fix: use NULL for deprecated plot argument defaults --- sess/R/hooks.R | 14 ++--- sess/R/server.R | 12 ++--- sess/README.md | 7 ++- sess/inst/tinytest/test-native-plot.R | 76 ++++++++++++++++++++++----- sess/man/connect.Rd | 8 +-- sess/man/register_hooks.Rd | 6 +-- 6 files changed, 89 insertions(+), 34 deletions(-) diff --git a/sess/R/hooks.R b/sess/R/hooks.R index 2c07e816a..fbc0ffc46 100644 --- a/sess/R/hooks.R +++ b/sess/R/hooks.R @@ -2,17 +2,17 @@ #' #' @param use_rstudioapi Logical. Enable rstudioapi emulation. #' @param use_httpgd Deprecated. Logical. Enable httpgd plot device if available. -#' Use `plot_backend` instead. +#' NULL means unspecified; legacy calls default to TRUE. Use `plot_backend` instead. #' @param use_jgd Deprecated. Logical. Enable jgd plot device if available. -#' Use `plot_backend` instead. +#' NULL means unspecified; legacy calls default to FALSE. Use `plot_backend` instead. #' @param plot_backend Plot backend: `auto`, `jgd`, `httpgd`, `standard`, or #' `native`. NULL also selects `auto`. Deprecated flags select the backend #' only when this argument is omitted. #' @export -register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE, +register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = NULL, use_jgd = NULL, plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) { - has_httpgd <- !missing(use_httpgd) - has_jgd <- !missing(use_jgd) + has_httpgd <- !is.null(use_httpgd) + has_jgd <- !is.null(use_jgd) .warn_deprecated_plot_args(has_httpgd, has_jgd) backend <- if (missing(plot_backend) && (has_httpgd || has_jgd)) { .legacy_plot_backend(use_httpgd, use_jgd) @@ -38,8 +38,8 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F } .legacy_plot_backend <- function(use_httpgd, use_jgd) { - if (is.na(use_httpgd)) use_httpgd <- TRUE - if (is.na(use_jgd)) use_jgd <- FALSE + if (is.null(use_httpgd) || is.na(use_httpgd)) use_httpgd <- TRUE + if (is.null(use_jgd) || is.na(use_jgd)) use_jgd <- FALSE if (use_jgd && use_httpgd) "auto" else if (use_jgd) "jgd" else if (use_httpgd) "httpgd" else "standard" } diff --git a/sess/R/server.R b/sess/R/server.R index 0804e5ae0..3faf0d007 100644 --- a/sess/R/server.R +++ b/sess/R/server.R @@ -4,9 +4,9 @@ #' If NULL, uses SESS_ENDPOINT, then SESS_DISCOVERY_FILE. #' @param use_rstudioapi Logical. Enable rstudioapi emulation. Defaults to TRUE. #' @param use_httpgd Deprecated. Logical. Use httpgd for plotting if available. -#' Defaults to TRUE. Use `plot_backend` instead. +#' NULL means unspecified; legacy calls default to TRUE. Use `plot_backend` instead. #' @param use_jgd Deprecated. Logical. Use jgd for plotting if available. -#' Defaults to FALSE. Use `plot_backend` instead. +#' NULL means unspecified; legacy calls default to FALSE. Use `plot_backend` instead. #' @param plot_backend Plot backend: `auto`, `jgd`, `httpgd`, `standard`, or #' `native`. NULL also selects `auto`. Deprecated flags select the backend #' only when this argument is omitted. @@ -18,11 +18,11 @@ #' string clears it, and an omitted field leaves it unchanged. Set #' `options(sess.quiet = TRUE)` to suppress the successful connection message. #' @export -connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE, - use_jgd = FALSE, +connect <- function(endpoint = NULL, use_rstudioapi = TRUE, use_httpgd = NULL, + use_jgd = NULL, plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) { - has_httpgd <- !missing(use_httpgd) - has_jgd <- !missing(use_jgd) + has_httpgd <- !is.null(use_httpgd) + has_jgd <- !is.null(use_jgd) .warn_deprecated_plot_args(has_httpgd, has_jgd) plot_backend <- if (missing(plot_backend) && (has_httpgd || has_jgd)) { .legacy_plot_backend(use_httpgd, use_jgd) diff --git a/sess/README.md b/sess/README.md index 5301dc2d7..019054fe7 100644 --- a/sess/README.md +++ b/sess/README.md @@ -53,7 +53,9 @@ sess::connect( If `plot_backend` is omitted, `sess::connect()` uses `auto`. The VS Code extension passes its configured backend explicitly. Calls that supply the deprecated `use_httpgd` or `use_jgd` arguments still use those values when `plot_backend` -is omitted. +is omitted. These deprecated arguments default to `NULL`, meaning unspecified. +When either is non-NULL, the unspecified flag uses its legacy default +(`use_httpgd = TRUE`, `use_jgd = FALSE`). If both are NULL, the backend is `auto`. If `endpoint` is omitted, `connect()` resolves it in this order: @@ -137,7 +139,8 @@ In VS Code, this is controlled by the `r.plot.backend` setting. hooks, plot task callbacks, and devices untouched. The `standard` backend continues to use the static plot viewer. When `plot_backend` is omitted, the existing `use_httpgd`/`use_jgd` arguments keep their previous meanings, but are -deprecated and warn when supplied explicitly. Use `plot_backend` for new code. +deprecated and warn when supplied with non-NULL values, including `FALSE`. +Use `plot_backend` for new code. ### Options and environment variables diff --git a/sess/inst/tinytest/test-native-plot.R b/sess/inst/tinytest/test-native-plot.R index a876291d9..6eae87a2e 100644 --- a/sess/inst/tinytest/test-native-plot.R +++ b/sess/inst/tinytest/test-native-plot.R @@ -83,19 +83,71 @@ local({ }) local({ - env <- new.env(parent = asNamespace("sess")) - env$connect <- sess::connect - environment(env$connect) <- env - env$.resolve_endpoint <- function(...) "" - warnings <- character() - invisible(capture.output(withCallingHandlers( - env$connect(use_httpgd = FALSE), - warning = function(w) { - warnings <<- c(warnings, conditionMessage(w)) - invokeRestart("muffleWarning") + # Exercise both public entry points without starting graphics or connecting + # to a server. NULL can also be forwarded by wrappers as an unspecified flag. + cases <- list( + list(args = list(), backend = "auto", deprecated = character()), + list(args = list(use_httpgd = NULL, use_jgd = NULL), backend = "auto", + deprecated = character()), + list(args = list(use_httpgd = TRUE), backend = "httpgd", deprecated = "use_httpgd"), + list(args = list(use_httpgd = FALSE), backend = "standard", deprecated = "use_httpgd"), + list(args = list(use_jgd = TRUE), backend = "auto", deprecated = "use_jgd"), + list(args = list(use_jgd = FALSE), backend = "httpgd", deprecated = "use_jgd"), + list(args = list(use_httpgd = NULL, use_jgd = TRUE), backend = "auto", + deprecated = "use_jgd"), + list(args = list(use_httpgd = FALSE, use_jgd = NULL), backend = "standard", + deprecated = "use_httpgd"), + list(args = list(use_httpgd = FALSE, use_jgd = TRUE), backend = "jgd", + deprecated = c("use_httpgd", "use_jgd")), + list(args = list(use_httpgd = FALSE, use_jgd = FALSE), backend = "standard", + deprecated = c("use_httpgd", "use_jgd")), + list(args = list(use_httpgd = NA, use_jgd = NA), backend = "httpgd", + deprecated = c("use_httpgd", "use_jgd")), + list(args = list(use_httpgd = FALSE, plot_backend = "native"), backend = "native", + deprecated = "use_httpgd"), + list(args = list(use_httpgd = FALSE, plot_backend = NULL), backend = "auto", + deprecated = "use_httpgd") + ) + for (entry_point in c("connect", "register_hooks")) { + env <- new.env(parent = asNamespace("sess")) + entry <- getExportedValue("sess", entry_point) + environment(entry) <- env + selected <- NULL + env$.legacy_plot_backend <- function(...) { + selected <<- sess:::.legacy_plot_backend(...) + selected + } + env$.resolve_plot_backend <- function(...) { + selected <<- sess:::.resolve_plot_backend(...) + selected } - ))) - expect_true(any(grepl("use_httpgd is deprecated", warnings, fixed = TRUE))) + env$runtime_start <- function(use_rstudioapi, plot_backend) { + selected <<- plot_backend + } + env$.resolve_endpoint <- function(...) "" + for (case in cases) { + selected <- NULL + warnings <- character() + withCallingHandlers( + do.call(entry, c(list(use_rstudioapi = FALSE), case$args)), + warning = function(w) { + msg <- conditionMessage(w) + if (entry_point != "connect" || + msg != "[sess] Connection info not available. Cannot connect to VS Code.") { + warnings <<- c(warnings, msg) + } + invokeRestart("muffleWarning") + } + ) + expect_equal(selected, case$backend) + expect_length(warnings, as.integer(length(case$deprecated) > 0L)) + if (length(case$deprecated)) { + expect_true(grepl(paste(case$deprecated, collapse = " and "), + warnings[[1L]], fixed = TRUE)) + expect_true(grepl("deprecated; use plot_backend", warnings[[1L]], fixed = TRUE)) + } + } + } }) # The public connection path stores native for discovery reconnects and still diff --git a/sess/man/connect.Rd b/sess/man/connect.Rd index f9449977e..4aa10b50d 100644 --- a/sess/man/connect.Rd +++ b/sess/man/connect.Rd @@ -7,8 +7,8 @@ connect( endpoint = NULL, use_rstudioapi = TRUE, - use_httpgd = TRUE, - use_jgd = FALSE, + use_httpgd = NULL, + use_jgd = NULL, plot_backend = c("auto", "jgd", "httpgd", "standard", "native") ) } @@ -19,10 +19,10 @@ If NULL, uses SESS_ENDPOINT, then SESS_DISCOVERY_FILE.} \item{use_rstudioapi}{Logical. Enable rstudioapi emulation. Defaults to TRUE.} \item{use_httpgd}{Deprecated. Logical. Use httpgd for plotting if available. -Defaults to TRUE. Use \code{plot_backend} instead.} +NULL means unspecified; legacy calls default to TRUE. Use \code{plot_backend} instead.} \item{use_jgd}{Deprecated. Logical. Use jgd for plotting if available. -Defaults to FALSE. Use \code{plot_backend} instead.} +NULL means unspecified; legacy calls default to FALSE. Use \code{plot_backend} instead.} \item{plot_backend}{Plot backend: \code{auto}, \code{jgd}, \code{httpgd}, \code{standard}, or \code{native}. NULL also selects \code{auto}. Deprecated diff --git a/sess/man/register_hooks.Rd b/sess/man/register_hooks.Rd index c141d3138..613a47a34 100644 --- a/sess/man/register_hooks.Rd +++ b/sess/man/register_hooks.Rd @@ -4,17 +4,17 @@ \alias{register_hooks} \title{Register VS Code runtime integrations} \usage{ -register_hooks(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE, +register_hooks(use_rstudioapi = TRUE, use_httpgd = NULL, use_jgd = NULL, plot_backend = c("auto", "jgd", "httpgd", "standard", "native")) } \arguments{ \item{use_rstudioapi}{Logical. Enable rstudioapi emulation.} \item{use_httpgd}{Deprecated. Logical. Enable httpgd plot device if available. -Use \code{plot_backend} instead.} +NULL means unspecified; legacy calls default to TRUE. Use \code{plot_backend} instead.} \item{use_jgd}{Deprecated. Logical. Enable jgd plot device if available. -Use \code{plot_backend} instead.} +NULL means unspecified; legacy calls default to FALSE. Use \code{plot_backend} instead.} \item{plot_backend}{Plot backend: \code{auto}, \code{jgd}, \code{httpgd}, \code{standard}, or \code{native}. NULL also selects \code{auto}. Deprecated From a5f505a9283d018d97bdef5d697eb61e5a2a1303 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 08:28:45 +0000 Subject: [PATCH 7/8] style: align plot argument regression assertion --- sess/inst/tinytest/test-native-plot.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sess/inst/tinytest/test-native-plot.R b/sess/inst/tinytest/test-native-plot.R index 6eae87a2e..e4e96aac0 100644 --- a/sess/inst/tinytest/test-native-plot.R +++ b/sess/inst/tinytest/test-native-plot.R @@ -143,7 +143,7 @@ local({ expect_length(warnings, as.integer(length(case$deprecated) > 0L)) if (length(case$deprecated)) { expect_true(grepl(paste(case$deprecated, collapse = " and "), - warnings[[1L]], fixed = TRUE)) + warnings[[1L]], fixed = TRUE)) expect_true(grepl("deprecated; use plot_backend", warnings[[1L]], fixed = TRUE)) } } From a624bd22e4ef22dd4e8beae4237842c83c56a4da Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 4 Oct 2026 08:57:31 +0000 Subject: [PATCH 8/8] fix: retain window scope for plot backend settings --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f7ba7538f..a5f1f2aa9 100644 --- a/package.json +++ b/package.json @@ -1860,7 +1860,8 @@ "r.plot.useHttpgd": { "type": "boolean", "default": false, - "scope": "machine-overridable", + "scope": "window", + "ignoreSync": true, "markdownDescription": "Legacy compatibility setting for selecting the httpgd plot viewer. When `#r.plot.backend#` is `auto`, setting this to `true` selects the `httpgd` backend. Workspace settings take precedence over user settings. Within the same scope, an explicit `#r.plot.backend#` value takes precedence.\n\nRequires the `httpgd` R package version 1.2.0 or later.", "markdownDeprecationMessage": "Deprecated: use `#r.plot.backend#` instead. This setting is retained for compatibility with existing configurations but will be removed in a future release.", "deprecationMessage": "Deprecated: use r.plot.backend instead. This setting is retained for compatibility with existing configurations but will be removed in a future release." @@ -1868,7 +1869,8 @@ "r.plot.backend": { "type": "string", "default": "auto", - "scope": "machine-overridable", + "scope": "window", + "ignoreSync": true, "enum": [ "auto", "standard",