SimpleRemote provides native-feeling SSH and Docker workspaces for Vim 9.1.
The Vim9 layer owns the target picker, virtual filesystem buffers, SimpleTree
integration, projections, commands, and public APIs. A small Rust runtime owns
local transport processes and is built by install.sh.
Plug 'beamiter/simpleremote', { 'do': './install.sh' }simpleremote-daemon agent --protocol json supervises the persistent remote
shell agent as a JSON bridge: Vim exchanges one JSON object per line and
the runtime performs every base64 step of the agent's line protocol
in-process. Before this, Vim spawned base64 twice per request; a directory
listing, a file read, a save or a SimpleFinder preview no longer costs a
process. Payloads that are not valid UTF-8 travel base64-encoded so latin-1
and binary files round-trip byte for byte. Without the runtime, Vim still
drives ssh/docker and base64 itself; an older runtime is detected through
simpleremote-daemon capabilities and used the way it always was.
The remote agent installs itself. Every connection ships the bundled
bin/simpleremote-agent.sh inside the launch command, compares it with the
installed copy and atomically replaces the copy only when it differs — a fresh
host or a plugin update never needs :SimpleRemoteInstallAgent.
For SSH targets the runtime uses a private deterministic OpenSSH ControlPath and ControlPersist, so the agent, probes, transfers, language servers, searches and terminals share one authenticated connection.
simpleremote-daemon exec is the shared stdio process boundary. SimpleCC uses
it to run pyright, basedpyright, or another language server in the active
remote workspace; SimpleFinder runs its searches through it; SimpleGit runs
git through it. The runtime replaces itself with the transport for exec
jobs, so stopping a Vim job stops the remote process too, and it prepends the
project .venv/bin, ~/.local/bin and friends. exec --tty allocates a
terminal, which :SimpleRemoteTerminal and SimpleTerminal use so remote
shells share the connection and the same PATH prelude.
simpleremote-daemon download and upload stream a file — or, with
--recursive, a directory through tar — across the boundary, staged beside
the destination and activated by rename; uploads refuse an existing
destination unless --force. Without the runtime, scp/docker cp do the
same job.
Set g:simpleremote_use_daemon = 0 to disable the runtime, or set
g:simpleremote_daemon_path to a custom build.
make suite-check loads the installed siblings alongside SimpleRemote,
connects a fixture-backed workspace and asserts that each one reacts — the
one test that exercises the contract instead of a stub of it. It is separate
from make check because that gate must pass in a checkout containing this
plugin alone.
Everything a sibling needs is a global function or a User event, all
feature-detected, none required. :help simpleremote-suite maps who uses
what; the short list:
g:SimpleRemoteShellCommand(script)/g:SimpleRemoteExecArgv()— run a shell script or an argv in the workspace root through the runtime.g:SimpleRemoteExecute(cmd, Cb),g:SimpleRemoteReadFile(path, Cb),g:SimpleRemoteWriteFile(path, content, Cb),g:SimpleRemoteListDirectory(path, Cb)— asynchronous remote filesystem operations over the persistent connection.g:SimpleRemoteDownload(remote, local, opts, Cb)/g:SimpleRemoteUpload(local, remote, opts, Cb)— file or directory transfers.g:SimpleRemoteTerminalSpec([cmd]),g:SimpleRemoteStatusline(),g:SimpleRemoteRecentWorkspaces(),g:SimpleRemoteProfiles(),g:SimpleRemoteOpenWorkspace(ws),g:SimpleRemoteSessionLines().- Events:
SimpleRemoteConnecting/Connected/WorkspaceChanged/RuntimeReady/ Disconnectedfor the connection,SimpleRemoteBufferReadfor buffers,SimpleRemoteFilesChangedfor tree/upload/API mutations,SimpleRemoteConfigChangedfor a reloaded remotesimplecc.json,SimpleRemoteFileCopied/FileUploadedfor transfers.
See :help simpleremote for commands, profiles, workspace projection, and the
integration API.
A remote read arrives as one reply holding the whole file, so a large one
costs the transport, the buffer and the request timeout at once — and because
the agent answers in order, everything queued behind it waits too. Opening a
file over g:simpleremote_large_file_bytes (10 MiB by default) therefore
transfers nothing: the buffer holds a hint naming the file and its size, is
not modifiable, and refuses :write. <CR> or :SimpleRemoteLoad reads it
into that same buffer, with a timeout scaled to the size so a deliberate load
is not cut off. Set the option to 0 to open everything on sight.
SimpleFinder detects the active remote workspace automatically. In a virtual
workspace its file, grep, interactive grep, word/visual grep, symbol, and Git
file sources execute through SimpleRemote's shared SSH/Docker transport;
results open as remote:// buffers and previews are read asynchronously over
the persistent agent. A mounted or explicitly mapped workspace keeps using
SimpleFinder's native local daemon against the projected local root.
Workspace changes refresh a live finder panel, while :SimpleFinderRoot
switches the real SimpleRemote workspace in the other direction. Set
g:simplefinder_remote = 0 to opt out.
g:SimpleRemoteTerminalSpec([command]) exposes an argv-based SSH/Docker
terminal specification for the active workspace. SimpleTerminal consumes it
so local and remote shells share one popup/session UI. When SimpleTerminal is
installed, :SimpleRemoteTerminal delegates to that UI automatically.
simpleremote-daemon probe reuses the SSH ControlMaster connection to report
the remote host, project root, Git, Python, Node, Python LSP, and round-trip
latency. SimpleRemote runs it asynchronously after connection, publishes it as
the probe member of g:simpleremote_workspace, and shows it through
:SimpleRemoteStatus. Use :SimpleRemoteProbe to refresh the snapshot.
Remote commands automatically prepend project .venv, .conda, venv, and
env tool directories plus $HOME/.local/bin and $HOME/bin. This makes uv,
project Python environments, and user-installed language servers available to
non-login SSH sessions without sourcing interactive shell files into LSP stdio.
SSH ControlMaster sockets live in a private 0700 directory below
$XDG_RUNTIME_DIR (or the per-user temporary fallback). Existing secure
directories are reused without a permission rewrite. The fallback is keyed by
the effective uid (not $USER), and symlinked, wrongly owned, or replaceable
runtime path components are rejected before a socket is created.
The virtual remote tree now follows almost the complete SimpleTree key vocabulary. Filesystem actions execute on the remote target:
c/xcollect the current node or marked nodes for remote copy/cut;ppastes them into the selected directory. Directories are recursive and every destination is collision-checked before the batch starts.a/ncreate a file,A/Ncreate a directory,rrenames, andDdeletes after confirmation. Delete refuses the tree root and any subtree containing an unsaved remote buffer.<Space>marks a node or a Visual range,gmmarks its visible siblings, andgMclears all marks. Copy, cut, and delete consume the marked set.mtoggles a persistent target-scoped bookmark,'lists bookmarks, and]b/[bcycle through visible bookmarks.
gd is the cross-boundary download: it streams the selected remote file — or
directory — through the Rust runtime into simpletree#ExternalDropDirectory()
(or prompts when no local tree exists), staged beside the destination and
atomically activated; SimpleTree then reveals the new file. gu is the
upload in the other direction: SimpleTree's selected node (or a prompted
local path) lands in the selected remote directory. y copies the file name,
Y copies the absolute remote path, and gy copies remote text file
contents subject to g:simpleremote_clipboard_max_bytes (1 MiB by default).
All copied text and resulting local paths use simpleclipboard#CopyText() when
available. Successful downloads emit User SimpleRemoteFileCopied with
g:simpleremote_event.remote and .local, then refresh SimpleTree. Set
g:simpleremote_copy_destination for a fixed local drop directory or
g:simpleremote_copy_prompt = 1 to confirm every download destination.
The remote tree also shares SimpleTree's root-navigation vocabulary: e uses
the selected directory, U moves to its parent up to /, and C accepts any
absolute remote directory. These explicit root changes now switch the real
SimpleRemote workspace, so projection discovery, the working directory,
remote config, and SimpleCC all follow the tree. The same synchronization runs
when a projected SimpleTree emits SimpleTreeRootChanged; SimpleRemote tags
its own simpletree#ExternalSetRoot() echo to avoid a reconnect loop. Set
g:simpleremote_sync_tree_root = 0 to retain detached browsing. . restores a
view-only reveal to the current workspace root.
Press ? in the virtual remote tree for a complete key reference; the
statusline keeps a visible [? keys] hint.
The virtual tree also mirrors SimpleTree navigation and view state: o and
arrow keys, S/V/t, Ctrl split keys, P preview, R refresh, H hidden
files, I gitignore filtering, s sort mode, gs reverse sort, F loaded
node filtering, z collapse all, f reveal active file, / find, and
]f/[f match cycling. L locks root-changing actions. The statusline shows
all active modes. Size and modification-time sorting use the bundled agent's
list-meta capability; update an older installed agent with
:SimpleRemoteInstallAgent. The only SimpleTree key in the documented panel
without a virtual equivalent is gx, because a remote path has no reliable
local system-default application.