diff --git a/CHANGELOG.md b/CHANGELOG.md index f8217b37..e9a74de7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +v3.0.0 of the R Extension for VS code is a major release. It introduces a +significant architectural change via the +[**`sess`**](https://github.com/REditorSupport/vscode-R/tree/master/sess) R +Package, which powers faster and more robust communication with the underlying R +session. In turn, this enables a variety of ancillary improvements and feature +requests, which we hope to continue building on. The extension will +automatically prompt users to install `sess` (on their behalf) if it is not +detected. + ### Bug Fixes * fix(rstudioapi): resolve emulation issues and viewer routing diff --git a/sess/.Rbuildignore b/sess/.Rbuildignore new file mode 100644 index 00000000..fb638aef --- /dev/null +++ b/sess/.Rbuildignore @@ -0,0 +1 @@ +^\.lintr$ diff --git a/sess/DESCRIPTION b/sess/DESCRIPTION index cc022f52..5955ddf1 100644 --- a/sess/DESCRIPTION +++ b/sess/DESCRIPTION @@ -1,13 +1,28 @@ Package: sess Type: Package -Title: Modern R IPC Server +Title: High-Performance IPC Bridge for R Sessions Version: 3.0.0 -Author: Gemini -Maintainer: Gemini -Description: Implements a high-performance IPC client for R sessions using Unix domain sockets and Windows named pipes. Replaces legacy file-system watcher based workflows while keeping JSON-RPC communication semantics for IDE/editor integration. -License: MIT +Authors@R: c( + person(given = "Randy", + family = "Lai", + role = c("aut", "cre"), + email = "randy.cs.lai@gmail.com"), + person(given = "Kun", + family = "Ren", + role = "ctb"), + person(given = "Grant", + family = "McDermott", + role = "ctb"), + person(given = "Tatsuya", + family = "Shima", + role = "ctb"), + person(given = "Fred", + family = "Wu", + role = "ctb") + ) +Description: Implements a high-performance IPC bridge for R sessions using Unix domain sockets and Windows named pipes. Replaces legacy file-system watcher based workflows while keeping JSON-RPC communication semantics for IDE/editor integration. +License: MIT + file LICENSE Encoding: UTF-8 -LazyData: true Imports: processx (>= 3.5.0), later, @@ -17,6 +32,7 @@ Imports: rstudioapi Suggests: bit64, + httpgd, jgd, svglite, tinytest diff --git a/sess/LICENSE b/sess/LICENSE new file mode 100644 index 00000000..c4a436bd --- /dev/null +++ b/sess/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2025 +COPYRIGHT HOLDER: REditorSupport diff --git a/sess/R/hooks.R b/sess/R/hooks.R index 0cb2dd90..c2000fbc 100644 --- a/sess/R/hooks.R +++ b/sess/R/hooks.R @@ -94,6 +94,21 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F ) # 3. Help System Interception + # Capture utils' own print methods before registering ours over them, so the + # fallbacks below do not need ::: to reach unexported internals. + if (is.null(.sess_env$orig_print_help_files)) { + .sess_env$orig_print_help_files <- utils::getS3method( + "print", "help_files_with_topic", + envir = asNamespace("utils") + ) + } + if (is.null(.sess_env$orig_print_hsearch)) { + .sess_env$orig_print_hsearch <- utils::getS3method( + "print", "hsearch", + envir = asNamespace("utils") + ) + } + sess_print.help_files_with_topic <- function(x, ...) { if (length(x) >= 1 && is.character(x)) { file <- x[1] @@ -104,7 +119,7 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F viewer = getOption("sess.helpPanel", "Two") )) } else { - utils:::print.help_files_with_topic(x, ...) + .sess_env$orig_print_help_files(x, ...) } invisible(x) } @@ -115,13 +130,14 @@ register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE, use_jgd = F sess_print.hsearch <- function(x, ...) { if (length(x) >= 1) { - requestPath <- paste0("/doc/html/Search?pattern=", tools:::escapeAmpersand(x$pattern)) + pattern <- gsub("&", "&", x$pattern, fixed = TRUE) + requestPath <- paste0("/doc/html/Search?pattern=", pattern) notify_client("help", list( requestPath = requestPath, viewer = getOption("sess.helpPanel", "Two") )) } else { - utils:::print.hsearch(x, ...) + .sess_env$orig_print_hsearch(x, ...) } invisible(x) } diff --git a/sess/R/rstudioapi.R b/sess/R/rstudioapi.R index 5b73e772..6ddce53b 100644 --- a/sess/R/rstudioapi.R +++ b/sess/R/rstudioapi.R @@ -353,7 +353,7 @@ serialize_location <- function(location) { } namespace_has <- function(obj, namespace) { - attempt <- try(getFromNamespace(obj, namespace), silent = TRUE) + attempt <- try(utils::getFromNamespace(obj, namespace), silent = TRUE) !inherits(attempt, "try-error") } diff --git a/sess/README.md b/sess/README.md index 60ca932e..f4fba911 100644 --- a/sess/README.md +++ b/sess/README.md @@ -1,262 +1,192 @@ -# `sess`: Modern R IPC Protocol +# sess — A high-performance IPC bridge for R sessions -The `sess` package provides an IPC layer between an R session and a client (such as the VS Code R extension). +`sess` is an R package for connecting your R sessions to an editor (client). The +[VS Code R extension](https://github.com/REditorSupport/vscode-R) is our primary +target client and `sess` powers many of the extension's core features: +workspace, data and plot viewers, help panel, hover and completion, RStudio API +emulation, etc. -Transport: +Under the hood, `sess` talks to the client over a local socket (Unix domain +socket on macOS/Linux, named pipe on Windows) using +[JSON-RPC 2.0](https://www.jsonrpc.org/specification) messages. -- Unix domain sockets (macOS/Linux) -- Windows named pipes +## Installation -Protocol: +> [!NOTE] +> +> ### Bundled install for VS Code +> +> Users of the VS Code R extension (>=v3.0.0) do not need to install `sess` +> manually. The extension bundles its own copy of `sess` and will install it +> for you (along with any missing CRAN dependencies) if it is missing or +> outdated. Managed R terminals ask first; attaching an existing session +> installs without prompting. -- JSON-RPC 2.0 messages -- JSON Lines (JSONL, newline-delimited JSON) framing (one JSON message per line) - -## 1. Connection Handshake - -Start the client connection from R: +`sess` is not yet on CRAN, but the development version can be installed from +GitHub: ```r -sess::connect( - pipe_path = NULL, # Character: pipe/socket path. NULL -> SESS_PIPE or session file fallback - use_rstudioapi = TRUE, # Logical: enable rstudioapi emulation - use_httpgd = TRUE # Logical: use httpgd for plotting if available -) -``` - -If `pipe_path` is omitted, `connect()` resolves it in this order: - -1. `SESS_PIPE` environment variable -2. `~/.vscode-R/sessions/{PID}.json` (`pipe` field) +# install.packages("remotes") +remotes::install_github("REditorSupport/vscode-R/sess") -After connecting, `sess` sends an `attach` notification. - -Example: - -```json -{ - "jsonrpc": "2.0", - "method": "attach", - "params": { - "version": "4.5.0", - "pid": 12345, - "tempdir": "/tmp/Rtmp.../sess", - "wd": "/path/to/project", - "info": { - "command": "/usr/bin/R", - "version": "R version 4.5.0 (...) ", - "start_time": "2026-05-05 06:00:00" - } - } -} +# or: pak::pak("REditorSupport/vscode-R/sess") ``` -## 2. Message Transport and Framing - -Transport uses JSON Lines (JSONL, newline-delimited JSON): - -- sender writes one JSON-RPC object + `\n` -- receiver buffers stream chunks and dispatches complete lines only - -This preserves JSON-RPC semantics while handling stream fragmentation safely. +If you prefer not to install from within an R session, you can install from +the terminal instead. A sparse clone fetches only the `sess` directory +(requires git >= 2.25): -## 3. JSON-RPC Message Types - -### Notification (one-way) - -```json -{ - "jsonrpc": "2.0", - "method": "method_name", - "params": {} -} +```sh +git clone --depth 1 --filter=blob:none --sparse https://github.com/REditorSupport/vscode-R.git +cd vscode-R +git sparse-checkout set sess +R CMD INSTALL sess ``` -### Request (expects response) +## Usage -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "method_name", - "params": {} -} -``` - -### Response (success) +When you start an R terminal from VS Code, the extension's R profile calls +`sess::connect()` for you. To connect a session yourself: -```json -{ - "jsonrpc": "2.0", - "id": 1, - "result": {} -} +```r +sess::connect( + pipe_path = NULL, # socket/pipe path; 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 +) ``` -### Response (error) +If `pipe_path` is `NULL`, `connect()` looks for it in this order: -```json -{ - "jsonrpc": "2.0", - "id": 1, - "error": { - "code": -32601, - "message": "Method not found" - } -} -``` +1. The `SESS_PIPE` environment variable. +2. The `pipe` field of `~/.vscode-R/sessions/{PID}.json`, a discovery file the + extension writes so that sessions can reattach after a window reload. -## 4. Notifications from R to Client +## What `sess` changes in your R session -`notify_client()` sends one-way events (no `id`), including: +Once connected, `sess` registers hooks (via `register_hooks()`) that redirect +R's interactive features to the client: -- `attach` -- `dataview` -- `plot_updated` -- `httpgd` -- `help` -- `browser` -- `webview` -- `restart_r` -- `send_to_console` +| R feature | Behavior | +|---|---| +| `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. | +| Graphics device | Plots appear in the editor's plot viewer (see below). | +| `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. | -## 5. Requests from R to Client (`request_client`) +### Graphics devices -`request_client()` sends JSON-RPC requests and waits for matching response `id`. +For displaying R plots, `sess` chooses a graphics device in this order: -Used by RStudio API emulation methods, such as: +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. +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). -- `rstudioapi/active_editor_context` -- `rstudioapi/replace_text_in_current_selection` -- `rstudioapi/insert_or_modify_text` -- `rstudioapi/show_dialog` -- `rstudioapi/navigate_to_file` -- `rstudioapi/set_selection_ranges` -- `rstudioapi/document_save` -- `rstudioapi/get_project_path` -- `rstudioapi/document_context` -- `rstudioapi/document_save_all` -- `rstudioapi/document_new` -- `rstudioapi/document_close` +In VS Code, this is controlled by the `r.plot.backend` setting. -Coordinate convention on the wire: +### Options and environment variables -- rows/columns are 1-indexed (R-style) -- client may convert to internal 0-indexed representation +| Name | Type | Purpose | +|---|---|---| +| `sess.helpPanel` | R option | View column for help pages (default `"Two"`). | +| `SESS_PIPE` | 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. | +| `JGD_SOCKET` | env var | Socket used by the jgd device; set by the extension. | -## 6. Requests from Client to R (Pull API) +## Protocol reference -Client queries R state through JSON-RPC requests. +This section is for developers writing or debugging a client. -### `workspace` +### Transport and framing -Request: +- **Transport:** Unix domain socket (macOS/Linux) or named pipe (Windows). + `sess` is the connecting side; the client listens. +- **Framing:** [JSON Lines](https://jsonlines.org/). Each message is one + JSON-RPC 2.0 object followed by `\n`. Receivers buffer incoming data and + dispatch complete lines only. +- **Messages:** standard JSON-RPC 2.0 notifications (no `id`), requests (with + `id`) and responses (`result` or `error`). Unknown request methods receive + error `-32601` (`Method not found`). +- **Coordinates:** row and column positions in `rstudioapi/*` messages are + 1-indexed, as in R. -```json -{"jsonrpc":"2.0","id":1,"method":"workspace","params":{}} -``` +### Handshake -Response (example): +On connecting, `sess` sends an `attach` notification: ```json { "jsonrpc": "2.0", - "id": 1, - "result": { - "globalenv": { - "my_df": {"class": ["data.frame"], "type": "list", "length": 11} - }, - "search": ["package:stats", "package:graphics"], - "loaded_namespaces": ["sess", "utils"] + "method": "attach", + "params": { + "version": "4.5.0", + "pid": 12345, + "tempdir": "/tmp/Rtmp.../sess", + "wd": "/path/to/project", + "info": { + "command": "/usr/bin/R", + "version": "R version 4.5.0 (...)", + "start_time": "2026-05-05 06:00:00" + } } } ``` -### `plot_latest` - -Request params example: - -```json -{"width":800,"height":600,"format":"svglite"} -``` - -Response example: - -```json -{"jsonrpc":"2.0","id":2,"result":{"format":"svglite","data":""}} -``` - -### `hover` - -Request params example: - -```json -{"expr":"head(mtcars)"} -``` - -Response example: - -```json -{"jsonrpc":"2.0","id":3,"result":{"str":"'data.frame': 6 obs. ..."}} -``` - -### `completion` - -Request params example: +### Notifications from R to client + +Sent with `notify_client()`. + +| Method | Params | Sent when | +|---|---|---| +| `attach` | see above | Connection is established. | +| `workspace_updated` | none | A top-level command completes. | +| `dataview` | `title`, `source`, `type`, and `view_id` (tables) or `file` (other objects) | `View()` is called. | +| `plot_updated` | none | The standard device records a new or changed plot. | +| `httpgd` | `url` | An httpgd device is opened. | +| `help` | `requestPath`, `viewer` | A help page or help search is printed. | +| `browser` / `webview` / `page_viewer` | `url` | The corresponding R viewer option is invoked. | +| `restart_r` | `command`, `clean` | `rstudioapi::restartSession()` is called. | +| `rstudioapi/send_to_console` | `code`, `execute`, `focus`, `animate` | `rstudioapi::sendToConsole()` is called. | + +### Requests from R to client + +Sent with `request_client()`, which blocks until the matching response +arrives. All are used by `rstudioapi` emulation: + +`rstudioapi/active_editor_context`, `rstudioapi/document_context`, +`rstudioapi/insert_or_modify_text`, +`rstudioapi/replace_text_in_current_selection`, +`rstudioapi/set_selection_ranges`, `rstudioapi/navigate_to_file`, +`rstudioapi/document_new`, `rstudioapi/document_save`, +`rstudioapi/document_save_all`, `rstudioapi/document_close`, +`rstudioapi/get_project_path`, `rstudioapi/show_dialog`, +`rstudioapi/ask_for_password`. + +### Requests from client to R + +| Method | Params | Result | +|---|---|---| +| `workspace` | none | `globalenv` (objects with `class`, `type`, `length`, ...), `search`, `loaded_namespaces` | +| `workspace_children` | `name`, `path`, `start` | `children`, `next_start` (paged expansion of lists, environments, S4/R6 objects) | +| `hover` | `expr` | `str`: the `str()` output of the evaluated expression | +| `completion` | `expr`, `trigger` (`$` or `@`) | Array of `{name, type, str}`, where `str` is the element's class | +| `plot_latest` | `width`, `height`, `format` (`svglite` or `png`), `devArgs` | `format`, `data` (base64) | +| `dataview_init` | `view_id` | `columns`, `totalRows` | +| `dataview_page` | `view_id`, `startRow`, `endRow`, `sortModel`, `filterModel` | `rows`, `totalRows`, `totalUnfiltered`, `lastRow` | +| `dataview_dispose` | `view_id` | `true` | + +Example exchange: ```json -{"expr":"mtcars","trigger":"$"} +{"jsonrpc":"2.0","id":4,"method":"completion","params":{"expr":"mtcars","trigger":"$"}} +{"jsonrpc":"2.0","id":4,"result":[{"name":"mpg","type":"double","str":"numeric"},{"name":"cyl","type":"double","str":"numeric"}]} ``` - -Response example: - -```json -{ - "jsonrpc": "2.0", - "id": 4, - "result": [ - {"name":"mpg","type":"double","str":"numeric"}, - {"name":"cyl","type":"double","str":"numeric"} - ] -} -``` - -## 7. Hook Registration and Options - -`connect()` initializes runtime hooks via `register_hooks()`. - -Intercepted features include: - -- `utils::View()` -- `browser()`, `viewer()`, `page_viewer()` -- help topic rendering hooks - -Relevant options: - -- `sess.row_limit` -- `sess.dataview` -- `sess.browser` -- `sess.webview` -- `sess.helpPanel` - -## 8. Discovery File - -To support reloads and attach workflows, the extension writes: - -- `~/.vscode-R/sessions/{PID}.json` - -`sess::connect()` reads this file as fallback when direct pipe parameters are unavailable. - -## 9. What Changed from the WebSocket Transport - -Changed: - -- transport is now UDS / named pipe -- framing is JSON Lines (JSONL) over stream sockets -- authentication token exchange is removed - -Unchanged: - -- JSON-RPC method names and payload shapes -- request/response correlation by `id` -- high-level feature behavior (workspace, hover, completion, plot, dataview, RStudio API emulation)