Add nvim server hopping with local and remote session support#309
Conversation
- connect_to() lets an empty launch dummy quit itself once its UI hops
away, so :connect no longer leaves orphan servers behind; plain
'nvim' launches pop the hop picker when other servers exist
(vim.g.nvim_server_autopick = false to disable)
- :NvimHop (<leader>ss): fzf-lua picker over live servers (labeled by
cwd) merged with zoxide dirs; <CR> connects or spawns a detached
headless server at the dir, ctrl-x kills a server
- :NvimRemote {host} / :NvimHop {host} (<leader>sr): spawn a detached
headless nvim on an ssh host (landing dir or remote zoxide dir) and
reach it through an ssh -L forwarded unix socket, so connect, rename
and kill work on remote servers exactly like local ones; ssh uses a
shared ControlMaster as the jump point and dead forwards are reaped
on listing
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017J2ZenpCfVCDmNN1M3mFSL
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 51cc05a0c5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| vim.cmd("qa!") | ||
| end | ||
| end, 1500) | ||
| local ok, err = pcall(vim.cmd, "connect " .. vim.fn.fnameescape(path)) |
There was a problem hiding this comment.
Forward remote targets before connecting
When this function is running inside a server that was reached through NvimRemote, path can be a socket path created or discovered on that remote host (for example, selecting a zoxide dir spawns a server there and then calls this function). :help :connect says the UI attaches to the server at {address}, so the local TUI tries to open that remote filesystem path locally; subsequent hops from a remote session therefore fail unless the target socket is forwarded back to the UI host first.
Useful? React with 👍 / 👎.
| if meta and meta.fwd_pid then | ||
| pcall(vim.uv.kill, meta.fwd_pid, "sigterm") |
There was a problem hiding this comment.
Validate the forward PID before killing it
When a forwarded Unix socket and .meta file survive after the ssh tunnel exits, the next server scan takes this path and sends SIGTERM to the recorded PID before removing the files. Since PIDs can be reused, this can terminate an unrelated process long after the original forward died; store enough identity to verify the process is still the expected ssh forward, or avoid killing by stale PID.
Useful? React with 👍 / 👎.
| local localsock = forward_remote(host, remote_sock) | ||
| if localsock then | ||
| connect_to(localsock) |
There was a problem hiding this comment.
Clean up remote servers after forward failures
If the remote nvim starts successfully but forward_remote() returns nil (for example because Unix-socket forwarding is disabled on the host or the local socket never appears), this callback just stops and leaves the headless remote server running at remote_sock with no local metadata or picker entry to reach it. Add a best-effort remote qa!/cleanup path when forwarding fails so failed connection attempts do not orphan remote sessions.
Useful? React with 👍 / 👎.
|
|
||
| local function remote_session(host, dir) | ||
| vim.notify("Spawning nvim on " .. host .. (dir and (" at " .. dir) or "") .. " ...") | ||
| local cmd = vim.list_extend(ssh_cmd(), { "-T", host, remote_spawn_script(dir) }) |
There was a problem hiding this comment.
Run the remote bootstrap with a POSIX shell
On SSH hosts whose login shell is not POSIX-compatible, such as fish or csh, this passes the generated script directly as the remote command, so that shell parses the ||, [ -S ... ], $((...)), and assignment syntax used by remote_spawn_script(). Those hosts fail before spawning nvim even when ssh and nvim are configured correctly; invoke sh -lc or otherwise force a POSIX shell for the bootstrap script.
Useful? React with 👍 / 👎.
Summary
This PR adds a comprehensive server hopping system for Neovim that enables seamless navigation between local and remote nvim instances, replacing manual tmux/sesh workflows. The implementation includes fuzzy picking over live servers and zoxide directories, SSH remote spawning with socket forwarding, and automatic cleanup of temporary sessions.
Key Changes
spawn_local_server()to create headless nvim instances at specified directories with automatic zoxide integrationremote_session()andforward_remote()to spawn nvim on SSH hosts and tunnel connections through local sockets with automatic cleanupopen_hop_picker()function providing fuzzy selection across live servers and zoxide directories (local or per-host), withctrl-xto kill servers~/.ssh/configis_disposable()to identify empty launch dummies that auto-quit when hopping away, preventing orphaned servers:NvimHop [host]- Fuzzy picker for servers/directories (local or on SSH host):NvimRemote {host}- Spawn and connect to a server on an SSH host:ShowNvimServers- Enhanced with metadata display<leader>ss- Local hop picker<leader>sr- SSH host selector then hop pickernviminvocation now offers hop picker if other servers exist (disable withvim.g.nvim_server_autopick = false)Implementation Details
.metafiles (JSON) to track SSH forwards and remote hostsnvim.{tag}.{pid}.{counter}in$XDG_RUNTIME_DIR/hop/or equivalent-Ntunneling withStreamLocalBindUnlinkfor clean socket managementvim.ui.selecthttps://claude.ai/code/session_017J2ZenpCfVCDmNN1M3mFSL