Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
run: install.packages("remotes")
shell: Rscript {0}
- run: npm run build
- name: Run sess tests
run: tinytest::test_package("sess")
shell: Rscript {0}
- name: Run tests (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a npm run test
Expand Down
2 changes: 1 addition & 1 deletion sess/DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ Suggests:
jgd,
svglite,
tinytest
Config/roxygen2/version: 8.0.0
Config/roxygen2/version: 8.1.0
8 changes: 6 additions & 2 deletions sess/R/dispatch.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ipc_write <- function(data) {
},
error = function(e) {
warning("[sess] Failed to send IPC message: ", e$message)
.transport_disconnect(silent = TRUE)
invisible(FALSE)
}
)
Expand Down Expand Up @@ -49,18 +50,21 @@ rpc_send <- function(method, params = list(), request = FALSE) {
msg$id <- req_id
}

ipc_write(msg)
sent <- ipc_write(msg)
if (!isTRUE(sent)) return(invisible(FALSE))

if (!request) {
invisible(TRUE)
} else {
# NON-BLOCKING WAIT:
# Run later callbacks (which include poll_connection) while waiting for a response.
while (is.null(.sess_env$pending_responses[[req_id]])) {
while (!is.null(.sess_env$con) && is.null(.sess_env$pending_responses[[req_id]])) {
later::run_now()
Sys.sleep(0.01)
}

if (is.null(.sess_env$con)) return(invisible(FALSE))

response <- .sess_env$pending_responses[[req_id]]
.sess_env$pending_responses[[req_id]] <- NULL

Expand Down
87 changes: 64 additions & 23 deletions sess/R/hooks.R
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
#' Register hooks for the client IPC
#' 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.
#' @export
register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE) {
runtime_start(use_rstudioapi, use_httpgd, use_jgd)
}

#' Start the VS Code runtime integration (internal)
#'
#' @keywords internal
runtime_start <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = FALSE) {
.sess_env$runtime_start_phase <- "initialize"
state <- .runtime_state()
if (isTRUE(state$active)) {
.sess_env$runtime_start_phase <- "previous-runtime-stop"
runtime_stop()
}
state <- .runtime_state()
state$active <- TRUE
completed <- FALSE
on.exit({
if (!completed) {
try(runtime_stop(), silent = TRUE)
} else {
.sess_env$runtime_start_phase <- NULL
}
}, add = TRUE)

# 1. Override View() to serve table data via paged RPC.
.sess_env$runtime_start_phase <- "view"
if (is.null(.sess_env$dataview_registry)) {
.sess_env$dataview_registry <- new.env(parent = emptyenv())
.runtime_set_field("dataview_registry", new.env(parent = emptyenv()))
}

show_dataview <- function(x, title = deparse(substitute(x))) {
Expand Down Expand Up @@ -58,9 +83,10 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
))
}
}
rebind("View", show_dataview, ns = "utils")
.runtime_rebind("View", show_dataview, ns = "utils")

# 2. Browser & Webview Options
.sess_env$runtime_start_phase <- "viewer-options"
make_viewer <- function(method) {
function(url, ...) {
if (!is.character(url)) {
Expand All @@ -86,14 +112,13 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
}
}

options(
browser = make_viewer("browser"),
viewer = make_viewer("webview"),
page_viewer = make_viewer("page_viewer"),
help_type = "html"
)
.runtime_set_option("browser", make_viewer("browser"))
.runtime_set_option("viewer", make_viewer("webview"))
.runtime_set_option("page_viewer", make_viewer("page_viewer"))
.runtime_set_option("help_type", "html")

# 3. Help System Interception
.sess_env$runtime_start_phase <- "help-s3"
sess_print.help_files_with_topic <- function(x, ...) {
if (length(x) >= 1 && is.character(x)) {
file <- x[1]
Expand All @@ -108,7 +133,7 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
}
invisible(x)
}
registerS3method(
.runtime_register_s3(
"print", "help_files_with_topic", sess_print.help_files_with_topic,
envir = asNamespace("utils")
)
Expand All @@ -125,11 +150,12 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
}
invisible(x)
}

# 4. Plot device: JGD > httpgd > Standard
.sess_env$runtime_start_phase <- "plot"
if (use_jgd && nzchar(Sys.getenv("JGD_SOCKET")) && requireNamespace("jgd", quietly = TRUE)) {
options(device = function(...) {
.runtime_set_option("device", function(...) {
jgd::jgd()
.runtime_track_device()
})

# On reattach (e.g. after a VS Code window reload) the renderer starts a new
Expand All @@ -145,16 +171,23 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
grDevices::dev.set(devs[names(devs) == "jgd"][[1]])
recorded <- tryCatch(grDevices::recordPlot(), error = function(e) NULL)
tryCatch(grDevices::dev.off(), error = function(e) NULL)
before_reopen <- grDevices::dev.list()
tryCatch(jgd::jgd(), error = function(e) NULL)
after_reopen <- grDevices::dev.list()
if (!is.null(after_reopen)) {
opened <- if (is.null(before_reopen)) after_reopen else setdiff(after_reopen, before_reopen)
if (length(opened)) .runtime_track_device(opened[[1L]])
}
if (!is.null(recorded)) {
tryCatch(grDevices::replayPlot(recorded), error = function(e) NULL)
}
invisible(TRUE)
}
reconnect_jgd_device()
} else if (use_httpgd && requireNamespace("httpgd", quietly = TRUE)) {
options(device = function(...) {
.runtime_set_option("device", function(...) {
httpgd::hgd(silent = TRUE)
.runtime_track_device()
notify_client("httpgd", list(url = httpgd::hgd_url()))
})
} else {
Expand Down Expand Up @@ -197,9 +230,10 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
}
}

options(device = function(...) {
.runtime_set_option("device", function(...) {
grDevices::pdf(NULL, width = 7, height = 7, bg = "white")
options(sess.null_dev = grDevices::dev.cur())
.runtime_track_device()
.runtime_set_option("sess.null_dev", grDevices::dev.cur())
grDevices::dev.control(displaylist = "enable")
})

Expand All @@ -215,7 +249,7 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
if (plot_updated || curr_length != last_plot_record_length) {
plot_updated <<- FALSE
last_plot_record_length <<- curr_length
.sess_env$latest_plot_record <- record
.runtime_set_field("latest_plot_record", record)
notify_client("plot_updated")
}
}
Expand All @@ -228,32 +262,39 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F
TRUE
}

setHook("plot.new", new_plot, "replace")
setHook("grid.newpage", new_plot, "replace")
.runtime_set_hook("plot.new", new_plot, "replace")
.runtime_set_hook("grid.newpage", new_plot, "replace")

update_plot()
addTaskCallback(update_plot, name = "sess.plot")
.runtime_add_task_callback(function(...) {
update_plot(...)
}, name = "sess.plot")
}

# 5. rstudioapi hooks
.sess_env$runtime_start_phase <- "rstudioapi"
if (use_rstudioapi) {
setHook(packageEvent("rstudioapi", "onLoad"), function(...) {
rstudioapi_hook <- function(...) {
patch_rstudioapi()
}, action = "append")
}
.runtime_set_hook(packageEvent("rstudioapi", "onLoad"),
rstudioapi_hook, action = "append")

if ("rstudioapi" %in% loadedNamespaces()) {
patch_rstudioapi()
}
}

# 6. Workspace Update Callback
.sess_env$runtime_start_phase <- "workspace-callback"
# This notifies the client whenever a top-level command is completed,
# suggesting that the Global Environment might have changed.
removeTaskCallback("sess.workspace")
addTaskCallback(function(...) {
.runtime_add_task_callback(function(...) {
if (!isTRUE(.runtime_state()$active)) return(FALSE)
notify_client("workspace_updated")
TRUE
}, name = "sess.workspace")

completed <- TRUE
invisible(NULL)
}
2 changes: 1 addition & 1 deletion sess/R/rstudioapi.R
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ patch_rstudioapi <- function() {

for (name in names(overrides)) {
if (exists(name, envir = asNamespace("rstudioapi"), inherits = FALSE)) {
rebind(name, overrides[[name]], "rstudioapi")
.runtime_rebind(name, overrides[[name]], "rstudioapi")
}
}
}
Loading
Loading