Adds seamless support for working with remote files in Neovim via SSH, with integrated Language Server Protocol (LSP) and TreeSitter support. This plugin handles the complexities of connecting remote language servers with your local Neovim instance, allowing you to work with remote projects as if they were local.
[!NOTE] This plugin takes a unique approach by running language servers on the remote machine while keeping the editing experience completely local. This gives you full LSP features without needing to install language servers locally.
This plugin takes a unique approach to remote development, given the currently available remote neovim plugins:
βββββββββββββββ SSH ββββββββββββββββ
β Neovim βββββββββββββΊβ Remote Host β
β (Local) β β β
β β β ββββββββββββ β
β βββββββββββ β β βLanguage β β
β β LSP β β β βServer β β
β β Client β β β β β β
β βββββββββββ β β β β β
βββββββββββββββ β ββββββββββββ β
ββββββββββββββββ
This approach gives you code editing and LSP functionality without network latency affecting editing operations.
:RemoteOpen rsync://user@host//path/to_folder/file.cpp
:RemoteTreeBrowser rsync://user@host//path/to_folder/
That's it! The plugin handles the rest automatically.
Language Server | Current support |
---|---|
C/C++ (clangd) | Fully supported β |
Python (pylsp) | Fully supported β |
Rust (rust-analyzer) | Not supported β |
Lua (lua_ls) | Fully supported β |
CMake (cmake) | Fully supported β |
XML (lemminx) | Fully supported β |
Zig (zls) | Not tested π‘ |
Go (gopls) | Not tested π‘ |
Java (jdtls) | Not tested π‘ |
JavaScript/TypeScript(tsserver) | Not tested π‘ |
C#(omnisharp) | Not tested π‘ |
Python (pyright) | Not supported β |
Bash (bashls) | Not supported β |
[!NOTE] If you find that desired LSP is not listed here, try testing it out, if it works (or not), open a GitHub issue and we can get it added to this list with the correct status
Platform | Support |
---|---|
Linux | β Full |
macOS | β Full |
Windows | π‘ WSL recommended |
Using lazy.nvim
{
"inhesrom/remote-ssh.nvim",
branch = "master",
dependencies = {
"inhesrom/telescope-remote-buffer", --See https://github.com/inhesrom/telescope-remote-buffer for features
"nvim-telescope/telescope.nvim",
"nvim-lua/plenary.nvim",
'neovim/nvim-lspconfig',
-- nvim-notify is recommended, but not necessarily required into order to get notifcations during operations - https://github.com/rcarriga/nvim-notify
},
config = function ()
require('telescope-remote-buffer').setup(
-- Default keymaps to open telescope and search open buffers including "remote" open buffers
--fzf = "<leader>fz",
--match = "<leader>gb",
--oldfiles = "<leader>rb"
)
-- setup lsp_config here or import from part of neovim config that sets up LSP
require('remote-ssh').setup({
on_attach = lsp_config.on_attach,
capabilities = lsp_config.capabilities,
filetype_to_server = lsp_config.filetype_to_server
})
end
}
Using packer.nvim
use {
'inhesrom/remote-ssh.nvim',
branch = "master",
requires = {
"inhesrom/telescope-remote-buffer",
"nvim-telescope/telescope.nvim",
"nvim-lua/plenary.nvim",
'neovim/nvim-lspconfig',
},
config = function()
require('telescope-remote-buffer').setup()
-- setup lsp_config here or import from part of neovim config that sets up LSP
require('remote-ssh').setup({
on_attach = lsp_config.on_attach,
capabilities = lsp_config.capabilities,
filetype_to_server = lsp_config.filetype_to_server
})
end
}
For seamless remote development, you need passwordless SSH access to your remote servers:
# Generate SSH key if you don't have one
ssh-keygen -t ed25519 -C "your_email@example.com"
# Copy key to remote server
ssh-copy-id user@remote-server
# Test passwordless connection
ssh user@remote-server
You'll need to configure LSP servers for the plugin to work properly. Here's a basic setup:
lsp_util.lua
):-- lsp_util.lua
local M = {}
-- LSP on_attach function with key mappings
M.on_attach = function(client, bufnr)
local nmap = function(keys, func, desc)
vim.keymap.set('n', keys, func, { buffer = bufnr, desc = desc })
end
-- Key mappings
nmap('gd', require('telescope.builtin').lsp_definitions, '[G]oto [D]efinition')
nmap('gr', require('telescope.builtin').lsp_references, '[G]oto [R]eferences')
nmap('gI', require('telescope.builtin').lsp_implementations, '[G]oto [I]mplementation')
nmap('K', vim.lsp.buf.hover, 'Hover Documentation')
nmap('<leader>rn', vim.lsp.buf.rename, '[R]e[n]ame')
nmap('<leader>ca', vim.lsp.buf.code_action, '[C]ode [A]ction')
end
-- LSP capabilities
local capabilities = vim.lsp.protocol.make_client_capabilities()
M.capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
-- Server definitions
M.servers = {
clangd = {}, -- C/C++
rust_analyzer = {}, -- Rust
pylsp = {}, -- Python
lua_ls = {}, -- Lua
-- Add more servers as needed
}
-- Generate filetype to server mapping
M.filetype_to_server = {}
for server_name, _ in pairs(M.servers) do
local filetypes = require('lspconfig')[server_name].document_config.default_config.filetypes or {}
for _, ft in ipairs(filetypes) do
M.filetype_to_server[ft] = server_name
end
end
return M
[!NOTE] You will need to manually ensure that the corresponding remote LSP is installed on the remote host
-- In your plugin configuration
{
'williamboman/mason.nvim',
dependencies = { 'williamboman/mason-lspconfig.nvim' },
config = function()
require('mason').setup()
require('mason-lspconfig').setup({
ensure_installed = vim.tbl_keys(require('lsp_util').servers),
})
end
}
Install the required language servers on your remote development machines:
# On remote server
pip3 install python-lsp-server[all]
# Optional: for better performance
pip3 install python-lsp-ruff # Fast linting
# Ubuntu/Debian
sudo apt install clangd
# CentOS/RHEL/Rocky
sudo dnf install clang-tools-extra
# macOS
brew install llvm
# Arch Linux
sudo pacman -S clang
# Install via rustup (recommended)
rustup component add rust-analyzer
# Or via package manager
# Ubuntu 22.04+: sudo apt install rust-analyzer
# macOS: brew install rust-analyzer
# Arch: sudo pacman -S rust-analyzer
# Ubuntu/Debian (if available in repos)
sudo apt install lua-language-server
# macOS
brew install lua-language-server
# Or install manually from releases:
# https://github.com/LuaLS/lua-language-server/releases
# Install Java first
sudo apt install openjdk-17-jdk # Ubuntu
brew install openjdk@17 # macOS
# jdtls will be automatically downloaded by Mason
# Install via pip
pip3 install cmake-language-server
# Or via package manager
sudo apt install cmake-language-server # Ubuntu 22.04+
Ensure your remote systems have the following:
# Check Python 3 availability
python3 --version
# Check rsync availability
rsync --version
# Verify SSH server is running
systemctl status ssh # Ubuntu/Debian
systemctl status sshd # CentOS/RHEL
# Test SSH access
ssh user@remote-server "echo 'SSH working'"
Here's a default configuration with comments explaining each option:
require('remote-ssh').setup({
-- Optional: Custom on_attach function for LSP clients
on_attach = function(client, bufnr)
-- Your LSP keybindings and setup
end,
-- Optional: Custom capabilities for LSP clients
capabilities = vim.lsp.protocol.make_client_capabilities(),
-- Custom mapping from filetype to LSP server name
filetype_to_server = {
-- Example: Use pylsp for Python (default and recommended)
python = "pylsp",
-- More customizations...
},
-- Custom server configurations
server_configs = {
-- Custom config for clangd
clangd = {
filetypes = { "c", "cpp", "objc", "objcpp" },
root_patterns = { ".git", "compile_commands.json" },
init_options = {
usePlaceholders = true,
completeUnimported = true
}
},
-- More server configs...
},
-- Async write configuration
async_write_opts = {
timeout = 30, -- Timeout in seconds for write operations
debug = false, -- Enable debug logging
log_level = vim.log.levels.INFO
}
})
# In your terminal
nvim rsync://user@remote-host/path/to/file.cpp
Or from within Neovim:
:e rsync://user@remote-host/path/to/file.cpp
:RemoteOpen rsync://user@remote-host/path/to/file.cpp
:RemoteTreeBrowser rsync://user@remote-host/path/to/directory
With telescope-remote-buffer, you get additional commands for managing remote buffers:
Default keymaps (configurable during setup as shown above):
<leader>fz
- Fuzzy search remote buffers<leader>gb
- Browse remote buffers<leader>rb
- Browse remote oldfilesThe plugin includes an intelligent file watching system that monitors remote files for changes made by other users or processes. This helps prevent conflicts and keeps your local buffer synchronized with the remote file state.
You can configure the file watcher behavior for each buffer, if you find the defaults are not working for you:
" Set poll interval to 10 seconds
:RemoteWatchConfigure poll_interval 10000
" Enable auto-refresh (automatically pull non-conflicting changes)
:RemoteWatchConfigure auto_refresh true
" Disable file watching for current buffer
:RemoteWatchConfigure enabled false
The file watcher supports SSH config aliases, allowing you to use simplified hostnames:
# ~/.ssh/config
Host myserver
HostName server.example.com
User myuser
Port 2222
Then use in Neovim:
:RemoteOpen rsync://myserver-alias//path/to/file.cpp
Note the double slash (//
) format which is automatically detected and handled.
Primary Commands | What does it do? |
---|---|
:RemoteOpen |
Open a remote file with scp:// or rsync:// protocol |
:RemoteTreeBrowser |
Browse a remote directory with tree-based file explorer |
:RemoteTreeBrowserHide |
Hide the remote file browser |
:RemoteTreeBrowserShow |
Show the remote file browser |
:RemoteGrep |
Search for text in remote files using grep |
:RemoteRefresh |
Refresh a remote buffer by re-fetching its content |
:RemoteRefreshAll |
Refresh all remote buffers |
File Watcher Commands | What does it do? |
---|---|
:RemoteWatchStart |
Start file watching for current buffer (monitors remote changes) |
:RemoteWatchStop |
Stop file watching for current buffer |
:RemoteWatchStatus |
Show file watching status for current buffer |
:RemoteWatchRefresh |
Force refresh from remote (overwrite local changes) |
:RemoteWatchConfigure |
Configure file watcher settings (enabled, poll_interval, auto_refresh) |
:RemoteWatchDebug |
Debug file watcher SSH connection and commands |
Debug Commands | What does it do? |
---|---|
:RemoteLspStart |
Manually start LSP for the current remote buffer |
:RemoteLspStop |
Stop all remote LSP servers and kill remote processes |
:RemoteLspRestart |
Restart LSP server for the current buffer |
:RemoteLspSetRoot |
Manually set the root directory for the remote LSP server, override automatic discovery |
:RemoteLspServers |
List available remote LSP servers |
:RemoteLspDebug |
Print debug information about remote LSP clients |
:RemoteLspDebugTraffic |
Enable/disable LSP traffic debugging |
:RemoteFileStatus |
Show status of remote file operations |
:AsyncWriteCancel |
Cancel ongoing asynchronous write operation |
:AsyncWriteStatus |
Show status of active asynchronous write operations |
:AsyncWriteForceComplete |
Force complete a stuck write operation |
:AsyncWriteDebug |
Toggle debugging for async write operations |
:AsyncWriteLogLevel |
Set the logging level (DEBUG, INFO, WARN, ERROR) |
:AsyncWriteReregister |
Reregister buffer-specific autocommands for current buffer |
:TSRemoteHighlight |
Manually enable TreeSitter highlighting for remote buffers |
Symptoms: No LSP features (completion, hover, etc.) in remote files
Solutions:
Check if language server is installed on remote:
ssh user@server "which clangd" # Example for clangd
ssh user@server "which rust-analyzer" # Example for rust-analyzer
Verify Mason installation locally:
:Mason
:MasonLog
Check LSP client status:
:LspInfo
:RemoteLspDebug
Enable LSP debug logging:
:RemoteLspDebugTraffic on
:LspLog
Symptoms: "Connection refused", "Permission denied", or timeout errors
Solutions:
Test basic SSH connectivity:
ssh user@server
Check SSH key authentication:
ssh-add -l # List loaded keys
ssh user@server "echo SSH key auth working"
Verify SSH config:
# Add to ~/.ssh/config
Host myserver
HostName server.example.com
User myuser
IdentityFile ~/.ssh/id_ed25519
Check remote SSH server status:
ssh user@server "systemctl status sshd"
Symptoms: Files won't open, save, or refresh
Solutions:
Check file permissions:
ssh user@server "ls -la /path/to/file"
Verify rsync availability:
ssh user@server "rsync --version"
Test file operations manually:
rsync user@server:/path/to/file /tmp/test-file
Check async write status:
:AsyncWriteStatus
:RemoteFileStatus
Symptoms: "Python not found" or proxy connection errors
Solutions:
Check Python 3 on remote:
ssh user@server "python3 --version"
ssh user@server "which python3"
Verify proxy script permissions:
ls -la ~/.local/share/nvim/lazy/remote-ssh.nvim/lua/remote-lsp/proxy.py
Check proxy logs:
ls -la ~/.cache/nvim/remote_lsp_logs/
Symptoms: No autocomplete suggestions in remote files
Solutions:
Check nvim-cmp configuration:
:lua print(vim.inspect(require('cmp').get_config()))
Verify LSP client attachment:
:LspInfo
Check LSP server capabilities:
:lua print(vim.inspect(vim.lsp.get_clients()[1].server_capabilities))
Symptoms: File watcher shows "not a remote buffer" or doesn't detect changes
Solutions:
Check if file watcher is running:
:RemoteWatchStatus
Test SSH connection manually:
:RemoteWatchDebug
Verify SSH config alias setup:
# Test SSH config alias
ssh myserver "echo 'SSH alias working'"
Check file watcher logs:
:AsyncWriteDebug # Enable debug logging
:AsyncWriteLogLevel DEBUG
Restart file watcher:
:RemoteWatchStop
:RemoteWatchStart
Symptoms: File watcher causing UI blocking or performance issues
Solutions:
Increase poll interval:
:RemoteWatchConfigure poll_interval 10000 # 10 seconds
Check for SSH connection multiplexing:
# Add to ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/control-%r@%h:%p
ControlPersist 10m
# LSP Debugging
:RemoteLspDebug # Show remote LSP client information
:RemoteLspServers # List available LSP servers
:RemoteLspDebugTraffic on # Enable LSP traffic debugging
:LspInfo # Show LSP client information
:LspLog # View LSP logs
# File Operation Debugging
:RemoteFileStatus # Show remote file operation status
:AsyncWriteStatus # Show async write operation status
:AsyncWriteDebug # Toggle async write debugging
# File Watcher Debugging
:RemoteWatchStatus # Show file watcher status for current buffer
:RemoteWatchDebug # Test SSH connection and debug file watcher
:RemoteWatchStart # Start file watching for current buffer
:RemoteWatchStop # Stop file watching for current buffer
# General Debugging
:checkhealth # General Neovim health check
:Mason # Open Mason UI for server management
:MasonLog # View Mason installation logs
Use SSH connection multiplexing:
# Add to ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/control-%r@%h:%p
ControlPersist 10m
Configure SSH keep-alive:
# Add to ~/.ssh/config
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Optimize rsync transfers:
# For large files, consider compression
Host myserver
Compression yes
While mounting remote directories (via SSHFS, etc.) is a valid approach, it has several drawbacks:
This plugin runs language servers directly on the remote machine where your code lives, providing a more responsive experience with full access to project context.
Neovim's built-in remote file editing doesn't provide LSP support. This plugin extends the built-in functionality by:
remote-nvim.nvim (https://github.com/amitds1997/remote-nvim.nvim) - The most VS Code Remote SSH-like solution:
distant.nvim (https://github.com/chipsenkbeil/distant.nvim) - Theoretically addresses latency:
This remote-ssh.nvim (https://github.com/inhesrom/remote-ssh.nvim):
:RemoteTreeBrowser
)The key trade-off is between feature completeness (remote-nvim.nvim) and responsiveness (this plugin's local buffer approach).
Contributions are welcome! Please read CONTRIBUTING.md for guidelines.
If you feel so inclined, out of appreciation for this work, send a coffee my way! Buy Me a Coffee Link
This project is licensed under the MIT License - see the LICENSE file for details.