This document describes the implementation of vscode-R-console.
The implementation is centered on four parts:
- the embedded R backend
- the transport model between the backend and the VS Code UI
- the
vscode-Rintegration layer - the self-managed console session layer
These are the main implementation references and the parts of this extension they informed. They are references unless the entry explicitly says the project is used as a dependency.
| Project | What is referenced or used | Where it appears here |
|---|---|---|
vscode-R |
Used directly for R executable settings, R/session/init.R, watcher files, attach/session metadata, and optional session-server member completion. |
src/Terminal/options.ts, resources/r/console-profile.R, src/Runtime/sessionWatcher.ts |
arf |
Reference for Rust embedded-R host structure, dynamic R loading, platform-specific R initialization, callback wiring, generic event/input-handler pumping, and interrupt state handling. | sidecar/pty-host/src/host.rs |
| Ark | Reference for native R frontend concepts, ReadConsole recovery after interrupts/nested input, nested-input separation, and generic R event/finalizer pumping while waiting for input. |
sidecar/pty-host/src/host.rs |
rchitect |
Reference for embedding R from a non-R host process, R home/shared-library discovery, and callback/FFI boundary patterns. | sidecar/pty-host/src/host.rs, src/Terminal/options.ts |
radian |
Reference for terminal-first console interaction, prompt-centric editing, multiline editing, history, reverse search, and bracketed paste expectations. | src/Terminal/rTerminal.ts and supporting terminal modules |
languageserver |
Used directly as the R language server package for completion, signature help, semantic tokens, and console virtual documents. | src/Language/consoleLspClient.ts, resources/r/console-language-server.R |
No Ark, arf, radian, or rchitect source files are vendored into this repository. The embedded backend, protocol, and terminal frontend are local implementations adapted from those ideas.
The embedded backend is the Rust sidecar binary:
- Unix:
bundled/bin/R_CONSOLE_HOST - Windows:
bundled/bin/R_CONSOLE_HOST.exe
Source files:
R_CONSOLE_HOST embeds R directly in its own process. It is not a wrapper
around another R terminal process, and it does not start a second session-host
binary.
On Windows, main.rs uses a launcher mode. The first process starts a detached
real host process using the same R_CONSOLE_HOST.exe binary. The detached
process is the process that loads R, owns the TCP control server, and owns the
console session.
Both Unix and Windows host modules maintain a shared state object for:
- queued top-level submissions
- queued nested replies
- parse-status requests
- dialog results
- pending width changes
- prompt/wait state
- busy state
- interrupt state
- shutdown state
- top-level recovery state
The backend command reader pushes protocol commands into this state. The R callbacks read from the same state when R asks for console input, writes output, requests dialogs, or reports busy state.
The Unix backend lives in the Unix module inside
sidecar/pty-host/src/host.rs.
It resolves R_HOME, then dynamically loads:
- Linux:
libR.so - macOS:
libR.dylib
The backend loads R symbols with libloading. Important symbols include:
Rf_initialize_Rsetup_Rmainlooprun_RmainloopR_ParseVectorR_tryEvalR_interrupts_pendingR_CheckUserInterruptR_ExpandFileName- optional event APIs such as
R_ProcessEvents,R_PolledEvents, andR_RunPendingFinalizers
Unix uses the normal embedded R callback globals:
ptr_R_ReadConsoleptr_R_WriteConsoleExptr_R_ShowMessageptr_R_Busyptr_R_Suicideptr_R_ChooseFileptr_R_EditFileptr_R_EditFilesptr_R_ProcessEventsR_PolledEvents
It also sets:
R_Outputfile = NULLR_Consolefile = NULL
That makes embedded R route console IO through the callback functions instead of file-backed console streams.
Unix initialization calls:
Rf_initialize_Rsetup_Rmainlooprun_Rmainloop
The backend adds --interactive when needed so embedded R is initialized as an
interactive console.
The Windows backend lives in the Windows module inside
sidecar/pty-host/src/host.rs.
It derives or uses R_HOME, then locates R.dll in this order:
R_HOME/bin/<R_ARCH>whenR_ARCHis set- the configured R executable directory
R_HOME/bin/x64R_HOME/bin/arm64R_HOME/bin
Before loading R.dll, the backend preloads support DLLs from the selected R
DLL directory:
Rgraphapp.dllRblas.dllRiconv.dllRlapack.dllR.dll
This preload order is part of the backend implementation because embedded R and some R packages rely on those DLLs resolving from the same R installation.
The Windows backend loads Windows-specific R symbols when present:
R_DefParamsR_DefParamsExR_SetParamscmdlineoptionsR_common_command_linereadconsolecfggetRUserGA_initappGA_peekeventR_ProcessEventsR_RunPendingFinalizersCharacterModeUserBreak- fallback
R_interrupts_pending
Windows initialization uses Rstart instead of the Unix
ptr_R_ReadConsole startup path:
- create an
Rstartstruct - initialize defaults through
R_DefParamsEx(..., 0)orR_DefParams - apply command-line options through
cmdlineoptionsandR_common_command_linewhen available - set interactive mode and callback pointers on
Rstart - set
rhomeandhome - call
R_SetParams - wire file/dialog callbacks
- call
GA_initapp - call
readconsolecfg - switch
CharacterModetoLinkDLL - call
setup_Rmainloop
Windows registers callbacks through the Rstart structure:
ReadConsoleWriteConsoleExShowMessageYesNoCancelBusyCallBackSuicideChooseFileEditFileEditFiles
Those callbacks bridge embedded R to the same shared backend state used by the Unix implementation.
The backend does not hardcode package event loops. It only calls generic R and platform event APIs.
Unix event pumping uses available symbols such as:
R_ProcessEventsR_checkActivityR_runHandlersR_InputHandlersR_PolledEventsR_RunPendingFinalizers
Windows event pumping uses:
- the Windows message queue through
PeekMessageW/DispatchMessageW GA_peekeventR_ProcessEventsR_checkActivityR_runHandlersR_InputHandlersR_RunPendingFinalizers
The backend runs these pumps while R is waiting in ReadConsole and from the
registered callback path. Input-handler draining is bounded per pump turn. If
an interrupt is requested during event processing, the backend preserves the R
interrupt flag and returns control to R.
ReadConsole is the main bridge between R and the VS Code UI.
The backend distinguishes:
- top-level console prompts: the locked
>and+prompts - nested prompts: every other prompt, including
readline()and browser-style prompts
Top-level input is supplied from queued submit commands. Nested input is
supplied from queued reply-input commands.
While ReadConsole is waiting, the backend also handles:
- parse-status requests
- width changes
- interrupts
- shutdown requests
- dialog results
- event pumping
After busy evaluation or nested input returns, the backend can insert one top-level recovery input before the next user command is consumed. The recovery step is implemented in the shared backend state and is generic R console handling; it does not inspect individual packages.
Unix interrupt implementation uses:
R_interrupts_pendingSIGINTsent to the current processR_CheckUserInterruptwhere needed
Windows interrupt implementation uses:
UserBreakwhen exported- otherwise
R_interrupts_pending R_CheckUserInterruptwhere needed
Both platforms install a base R interrupt calling handler on the first real
top-level console read. That handler uses R's abort restart so R can unwind
through normal cleanup paths.
Windows does not use GenerateConsoleCtrlEvent.
TypeScript transport implementation:
Rust transport implementation:
sidecar/pty-host/src/protocol.rs- the session command reader in
sidecar/pty-host/src/host.rs
When a console starts, RustSidecarRuntimeBackend.start(...):
- creates a UUID session id
- creates a per-session bootstrap file path
- spawns
R_CONSOLE_HOST - passes
VSC_R_BACKEND_SESSION_FILEto the backend - passes initial connect and reconnect grace values
- detaches/unrefs the process from the VS Code extension host
The Rust host reads VSC_R_BACKEND_SESSION_FILE, binds a TCP listener on
127.0.0.1:0, then writes JSON to the bootstrap file:
{"port":12345,"pid":67890}The TypeScript side polls that file, reads the port and pid, then connects to the loopback server.
Reconnect uses RuntimeSessionReconnectInfo:
sessionIdportpid
RustSidecarRuntimeBackend.reconnect(...) creates a new
RuntimeSessionHandle around that information. It does not spawn a new R
backend. It connects to the existing loopback TCP server.
The runtime checks process liveness before reconnecting:
- if the recorded backend pid is dead, the reconnect info is discarded
- if the Windows launcher process has exited but the detached real host pid is alive, reconnect continues
The Rust backend accepts one active control client at a time.
Implementation details:
- each accepted socket receives a client id
current_clientstores the currently active id- a new socket replaces the previous active socket
- command-reader threads exit when their client id is no longer current
- output is attached to the active client writer
- when the active client disconnects, output is detached and the reconnect grace timer is applied
This is how a console tab can detach and another UI instance can attach to the same embedded R backend.
The protocol frame header is 12 bytes:
- payload length
- frame kind
- flags
- request id
Backend events sent to TypeScript include:
backend-readyhost-connectedsession-statepromptbusyinput-requestinput-enddialog-requestoutput-flushparse-status-resulthost-error
Commands sent to Rust include:
submitreply-inputinterruptset-widthdialog-resultshutdown
Host capabilities are advertised through backend-ready:
control-channelshutdownsession-controltop-level-submitnested-inputparse-statusset-width
Rust emits console output and control events through the active protocol writer.
If no client is attached, protocol.rs queues frames in an output backlog.
The backlog is bounded by:
- maximum buffered frames
- maximum buffered bytes
When a client attaches, the backend:
- attaches the socket writer
- flushes the backlog
- emits
backend-ready - emits
host-connected - emits
session-state - emits
output-flush
Attached sessions close by writing a shutdown frame over the active socket.
Detached sessions close by:
- constructing a temporary runtime backend in the extension host
- reconnecting to the stored
sessionId/port/pid - writing the same
shutdownframe
The Rust command reader maps shutdown to the backend shared shutdown state.
The console depends on vscode-R, but it does not own or manage the
vscode-R session.
Implementation files:
R executable selection reads the vscode-R settings:
r.rpath.windowsr.rpath.macr.rpath.linux
If those are unset, the console falls back to ambient R_HOME, then R on
PATH.
The selected executable is used to derive:
R_HOMER_SHARE_DIRR_INCLUDE_DIRR_DOC_DIR- platform loader paths
The same selected R executable is used for:
- the embedded backend
- the console
languageserverprocess
The console requires:
REditorSupport.r/R/session/init.R
options.ts resolves the installed REditorSupport.r extension path through
the VS Code extension API. Startup is rejected if init.R is missing.
The backend launch environment includes:
VSCODE_INIT_RVSCODE_WATCHER_DIRR_PROFILE_USER_OLDVSC_R_EXECUTABLE
At launch time, rTerminal/runtime.ts also sets:
R_PROFILE_USERto this extension'sconsole-profile.RVSC_R_COLSVSC_R_ROWSVSC_R_SESSION_CWDVSC_R_EXT
console-profile.R then:
- restores and sources the user's original profile through
R_PROFILE_USER_OLD - sources
VSCODE_INIT_R - for R 4.6 compatibility, bridges vscode-R's legacy global
.First.sysdeferred attach hook through.FirstwhenVSCODE_INIT_Rinstalls it, preserving user.First()startup logic before vscode-R attach - installs the console pager
- locks the prompt options used by the embedded console contract
SessionWatcher reads files produced by vscode-R under
VSCODE_WATCHER_DIR.
It watches:
- root
request.lock - root
request.log - session
workspace.lock - session
workspace.json - the attached session directory until workspace state appears
From request.log, it reads:
- attach/detach commands
- R session tempdir
- R session pid
- optional session server host/port/token
From workspace.json, it reads:
- search path
- loaded namespaces
- global environment summary
The watcher can auto-pin to the first fresh attach event when the backend pid is not known yet. Once pinned, it ignores attach events from other R sessions.
The console consumes watcher metadata for:
- display pid selection
- search-path aware completion
- global-environment completion
$and@member completion through thevscode-Rsession server when available- attached package and namespace sync into the console LSP
This integration is read-only with respect to vscode-R sessions. The console
does not create, replace, restore, or stop vscode-R sessions.
Self-managed console implementation is split between:
The self-managed console is the extension's own embedded backend session. It is
separate from the vscode-R session watcher.
runtimeBackend.ts exposes:
start(args, options)reconnect(info)attach(session, handlers)detach(session)sendSessionCommand(session, command)requestParseStatus(session, code)close(session)isAlive(session)getPid(session)getReconnectInfo(session)
RTerminal stores the returned RuntimeSessionHandle as its backend session.
The handle is used for all protocol commands and for exporting reconnect state.
The extension stores self-managed console records in:
persistent-sessions.json
The file is stored under extension storage:
context.storageUriwhen available- otherwise
context.globalStorageUri
Each record contains:
- runtime reconnect info: session id, port, pid
- persisted terminal options: R path, R args, watcher settings, bracketed paste, cwd
- UI replay state: replay buffer, dimensions, mode, prompt state, input text, cursor position, backend pid
- terminal location: panel or editor column
The persisted data belongs to R Console. It is not a vscode-R session
snapshot.
On activation, extension.ts loads persistent-sessions.json.
Records are validated before use. A record is kept only when:
- its runtime reconnect info is structurally valid
- its terminal options are structurally valid
- its UI replay state is structurally valid
- its recorded backend pid is either absent or still alive
Dead records are removed from the in-memory registry and the persisted file is rewritten.
Creating a console follows this implementation path:
createRTerminal(...)resolvesRTerminalOptionsnew RTerminal(...)creates the terminal controllerattachTerminal(...)creates a VS Code pseudoterminalRTerminalcalls the runtime backend to start a sessionruntimeBackend.tsstartsR_CONSOLE_HOST- the backend writes the bootstrap file
- TypeScript connects to the loopback server
- backend events update terminal state
- the extension schedules persistence of the session record
Attaching a detached console follows this implementation path:
- the session manager reads the persisted record
buildPersistentTerminalOptions(...)combines current resolved options with persisted optionsnew RTerminal(options, extensionPath, persistedState)rebuilds the frontend- the persisted runtime reconnect info is passed to
runtimeBackend.reconnect - TypeScript connects to the existing backend socket
- the terminal replay state is used to rebuild the visible terminal
No new embedded R backend is started for a reconnect.
Attached console detach persists the latest reconnect/UI state, disconnects the frontend socket, disposes the VS Code terminal UI, and leaves the backend session running for the next session-manager attach.
Attached console close uses the active RTerminal and active runtime session.
Detached console close uses the persisted reconnect info:
- create a runtime backend object
- call
reconnect(...)with the persisted runtime info - call
close(...) - remove the persistent registry record
The close command targets the R Console backend socket. It does not send any
command to vscode-R.
The extension persists console records:
- after console creation
- after attach/detach state changes
- after terminal title/pid updates
- on a debounce timer
- on a heartbeat timer
- during extension shutdown
RTerminal.exportPersistentState() is the source of the persisted runtime and
UI state. If the runtime backend cannot provide reconnect info, the terminal is
not persisted.
src/extension.tssrc/Runtime/runtimeBackend.tssrc/Runtime/backendProtocol.tssrc/Terminal/rTerminal.tssrc/Terminal/rTerminal/runtime.tssrc/Terminal/options.tssrc/Runtime/sessionWatcher.tsresources/r/console-profile.Rsidecar/pty-host/src/host.rssidecar/pty-host/src/protocol.rssidecar/pty-host/src/main.rssrc/Language/consoleLspClient.tsresources/r/console-language-server.R