From da31527b6c1ba3cb12280515489407cfbbb44f2f Mon Sep 17 00:00:00 2001 From: Randy Lai Date: Mon, 9 Mar 2026 23:22:32 -0700 Subject: [PATCH 01/47] docs: add documentation for R and VS Code IPC mechanisms --- session.md | 650 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 650 insertions(+) create mode 100644 session.md diff --git a/session.md b/session.md new file mode 100644 index 00000000..7f6cc950 --- /dev/null +++ b/session.md @@ -0,0 +1,650 @@ +# R and VS Code Inter-Process Communication (IPC) + +The communication between the R session and VS Code involves two primary mechanisms: + +1. **R to VS Code (Asynchronous)**: File-based IPC mechanisms used to push data or trigger UI actions in VS Code. It uses specific pairs of data/log and lock files: + - **Command Dispatch:** `request.log` and `request.lock` + - **Workspace State:** `workspace.json` and `workspace.lock` + - **Static Plots:** `plot.png` and `plot.lock` +2. **VS Code to R (Synchronous)**: HTTP Server IPC where R runs a local `httpuv` server that VS Code queries. + +Here are the relevant code snippets for each mechanism. + +--- + +## 1. R to VS Code (File-based IPC) + +When R wants to push data or trigger UI actions in VS Code, it writes a payload or image to a temporary file and updates a corresponding lock file to notify VS Code's file system watcher. + +### 1.1 Command Dispatch (`request.log`) + +The base function used to send requests from R is `request()`. It writes the command and arguments to `request_file` and updates `request_lock_file` with a timestamp. + +#### R Component ([`R/session/vsc.R`](R/session/vsc.R)) + +```r +get_timestamp <- function() { + sprintf("%.6f", Sys.time()) +} + +request <- function(command, ...) { + obj <- list( + time = Sys.time(), + pid = pid, + wd = wd, + command = command, + ... + ) + jsonlite::write_json(obj, request_file, + auto_unbox = TRUE, null = "null", force = TRUE + ) + cat(get_timestamp(), file = request_lock_file) +} +``` + +R uses this core `request` function to send 8 distinct types of commands to VS Code. Below are snippets representing how each command is triggered from R: + +**1. webview**: Opens a local HTML file in a VS Code Webview panel (e.g., for htmlwidgets). [-> `show_webview`](R/session/vsc.R#L729) + +```r +show_webview <- function(url, title, ..., viewer) { + if (file.exists(url)) { + file <- normalizePath(url, "/", mustWork = TRUE) + request("webview", file = file, title = title, viewer = viewer, ...) + } +} +``` + +**2. browser**: Opens an external URL or help page via VS Code. [-> `request_browser`](R/session/vsc.R#L675) + +```r +request_browser <- function(url, title, ..., viewer) { + request("browser", url = url, title = title, ..., viewer = viewer) +} +``` + +**3. dataview**: Opens the built-in VS Code data viewer (triggered by `View()`). + +```r +# Inside dataview handler for a table/dataframe +file <- tempfile(tmpdir = tempdir, fileext = ".json") +jsonlite::write_json(data, file, na = "string", null = "null", auto_unbox = TRUE, force = TRUE) +request("dataview", source = "table", type = "json", + title = title, file = file, viewer = viewer, uuid = uuid +) +``` + +**4. httpgd**: Notifies VS Code that an `httpgd` plot graphics device is ready. + +```r +.vsc$request("httpgd", url = httpgd::hgd_url()) +``` + +**5. attach**: Called when the R session starts and attaches to VS Code, providing session metadata and the `SessionServer` credentials. [-> `attach`](R/session/vsc.R#L639) + +```r +attach <- function() { + request("attach", + version = sprintf("%s.%s", R.version$major, R.version$minor), + tempdir = tempdir, + info = list(...) + ) +} +``` + +**6. detach**: Called when the R session is closing to clean up resources. + +```r +reg.finalizer(.GlobalEnv, function(e) .vsc$request("detach"), onexit = TRUE) +``` + +**7. help**: Opens the help documentation panel inside VS Code. + +```r +request(command = "help", requestPath = requestPath, viewer = viewer) +``` + +**8. rstudioapi**: Emulates an `rstudioapi` request synchronously. It waits for VS Code to respond using a `response.lock` polling mechanism. + +```r +request_response <- function(command, ...) { + request(command, ..., sd = dir_session) + wait_start <- Sys.time() + while (!get_response_lock()) { + # Loop and sleep waiting for VS Code to reply + } +} +# (Dispatches to request_response with command = "rstudioapi") +``` + +#### TypeScript Component ([`src/session.ts`](src/session.ts)) + +VS Code uses `fs.watch` to monitor the `request.lock` file. When modified, it reads `request.log` and dispatches the command. + +```typescript +export function startRequestWatcher(sessionStatusBarItem: StatusBarItem): void { + requestFile = path.join(homeExtDir(), 'request.log'); + requestLockFile = path.join(homeExtDir(), 'request.lock'); + requestTimeStamp = 0; + + // Watch the lock file for changes + fs.watch(requestLockFile, {}, () => { + void updateRequest(sessionStatusBarItem); + }); +} +``` + +Inside `updateRequest`, the JSON payload is parsed and the specific `command` is passed to its respective handler function. Here is a detailed breakdown of how each command is resolved in VS Code: + +**1. `webview` Handler** + +```typescript +case 'webview': { + if (request.file && request.title && request.viewer !== undefined) { + await showWebView(request.file, request.title, request.viewer); + } + break; +} +``` + +**Detail (`showWebView` [-> src/session.ts#L313](src/session.ts#L313)):** +This handles local HTML content, most notably interactive `htmlwidgets`. + +```typescript +export async function showWebView(file: string, title: string, viewer: string | boolean): Promise { + if (viewer === false) { + void env.openExternal(Uri.file(file)); + } else { + const dir = path.dirname(file); + const webviewDir = extensionContext.asAbsolutePath('html/session/webview/'); + const panel = window.createWebviewPanel('webview', title, + { preserveFocus: true, viewColumn: ViewColumn[String(viewer) as keyof typeof ViewColumn] }, + { + enableScripts: true, enableFindWidget: true, retainContextWhenHidden: true, + localResourceRoots: [Uri.file(dir), Uri.file(webviewDir)], + }); + panel.iconPath = new UriIcon('globe'); + panel.webview.html = await getWebviewHtml(panel.webview, file, title, dir, webviewDir); + } +} +``` + +1. **Fallback to OS Browser**: If `viewer` is explicitly `false`, it bypasses VS Code and opens the file in the OS's default external browser via `env.openExternal(Uri.file(file))`. +2. **Creating the Panel**: Otherwise, `showWebView()` uses the VS Code API `window.createWebviewPanel()` to instantiate a native panel. +3. **Security and Access**: It explicitly restricts file system access by setting `localResourceRoots`. It only allows the webview to read assets from two places: the directory containing the HTML file itself (so local CSS/JS dependencies can load), and the extension's `html/session/webview/` directory. +4. **HTML Injection**: It calls `getWebviewHtml()` ([-> src/session.ts#L651](src/session.ts#L651)), which reads the target HTML file into a string. Crucially, it uses a regular expression to rewrite all relative `href` and `src` paths into secure `vscode-webview-resource://` URIs by prepending `webview.asWebviewUri(Uri.file(dir))`. Finally, it wraps the content with a Content Security Policy (CSP) and injects a custom `observer.js` script to handle layout updates, before assigning the final string to `panel.webview.html`. + +**2. `browser` Handler** + +```typescript +case 'browser': { + if (request.url && request.title && request.viewer !== undefined) { + await showBrowser(request.url, request.title, request.viewer); + } + break; +} +``` + +**Detail (`showBrowser` [-> src/session.ts#L225](src/session.ts#L225)):** +This handles external URLs and active R servers (like the R help server). + +```typescript +export async function showBrowser(url: string, title: string, viewer: string | boolean): Promise { + const uri = Uri.parse(url); + if (viewer === false) { + void env.openExternal(uri); + } else { + const externalUri = await env.asExternalUri(uri); + const panel = window.createWebviewPanel('browser', title, + { preserveFocus: true, viewColumn: ViewColumn[String(viewer) as keyof typeof ViewColumn] }, + { enableFindWidget: true, enableScripts: true, retainContextWhenHidden: true }); + + // event listeners for activeBrowserPanel omitted for brevity + + panel.iconPath = new UriIcon('globe'); + panel.webview.html = getBrowserHtml(externalUri); // Injects