Neovim plugin that allows to seamlessly run LSP servers in devcontainers.
- Run LSP server in devcontainer while Neovim is running on host system
- Translate paths within LSP
root_dirbetween container paths and host system paths - Translate paths outside of
root_dirallowing for seamlesstextDocument/definitionto system files inside container - Automatically start devcontainer if
.devcontainer/exists inroot_dir - Delay LSP server start until devcontainer is ready
- Compatible with new
vim.lsp.configAPI - Compatible with the legacy nvim-lspconfig
on_new_configAPI
There are multiple approaches to using devcontainers with text editor.
VSCode acts by installing and starting a headless instance of itself inside the container and then connecting a frontend to it from the host system. This approach limits usability of the editor for things not related to the container and requires modifying the container to install the server. Similar apprach is taken by esensar/nvim-dev-container and arnaupv/nvim-devcontainer-cli.
This plugin takes an alternative approach. Neovim runs on host system, so you have your full configuration and
access to the whole system. However, when starting an LSP server, instead of running it on host system, it runs inside
the devcontainer itself (using devcontainer exec ...). Then devcontainers.nvim intercepts the whole RPC communication
between LSP client (nvim) and server. All URIs in RPC messages are translated based on devcontainer workspaceFolder
such that both the client and the server see correct paths for their corresponding environments.
Below is a simplified diagram:
flowchart LR
subgraph Neovim
client["LSP Client"] <-- RPC --> mapping["Path mapping"]
end
mapping <-- RPC --> server["LSP server"]
subgraph Devcontainer
server <--> containerFs["Container files"]
end
Efficient identification of URIs in LSP messages is possible by utilizing the metaModel.json provided in LSP specification. devcontainers.nvim parses this file and creates a lookup of URI positions by message type, so there is no need to recursively iterate over all data and match uris by regex.
Install dependencies:
- Neovim nightly (not tested on stable but might work)
- devcontainers-cli
Configure the plugin using plugin manager of choice, e.g. lazy.nvim
{
'jedrzejboczar/devcontainers.nvim',
dependencies = {
'netman.nvim', -- optional to browse files in docker container
},
opts = {},
}For full configuration options see devcontainers/config.lua.
Optional dependencies:
- netman.nvim (or some other plugin) - needed in order to open files from the container (files not mounted on host system); the plugin must provide BufReadCmd for buffers with
docker://scheme - overseer.nvim - if available, then will be used to run
devcontainer upas overseer task, this allows to view output in buffer, stop/restart, etc.
To use devcontainers.nvim with the new built-in Neovim vim.lsp.config API you just need to change the cmd
in configuration table, e.g.
vim.lsp.config('clangd', { cmd = require('devcontainers').lsp_cmd({ 'clangd' }) })Or if you already have some config defined in lsp/*.lua file under runtimepath (these files are now e.g.
provided by nvim-lspconfig), you can wrap cmd defined there, e.g.
vim.lsp.config('clangd', { cmd = require('devcontainers').lsp_cmd(vim.lsp.config.clangd.cmd) })You can also define different cmd to use on per-directory basis using
require('devcontainers.local_cmd').set(root_dir, client_name, cmd).
This can also be put in .nvim.lua instead your main Neovim config
(see 'exrc').
For example, suppose that you have some default cmd = { 'clangd' } already defined, but if you start LSP
root_dir matches /some/dir then you want to have different command:
vim.lsp.config('clangd', { cmd = require('devcontainers').lsp_cmd { 'clangd' } })
require('devcontainers.local_cmd').set('/some/dir', 'clangd', { 'clangd', '--query-driver=/usr/bin/arm-none-eabi-*' })This can be combined with 'exrc'. Ensure that 'exrc' is enabled
(set exrc), then /some/dir/.nvim.lua:
local dir = '/some/dir' -- or by parsing `debug.getinfo(1, 'S').source`
require('devcontainers.local_cmd').set(dir, 'clangd', { 'clangd', '--query-driver=/usr/bin/arm-none-eabi-*' })This is still supported but considered legacy API.
on_new_configis deprecated in nvim-lspconfig so users are advised to use the new built-invim.lsp.configAPI.
The simplest setup is to enable devcontainers.nvim for all LSP clients:
local lspconfig_util = require('lspconfig.util')
lspconfig_util.on_setup = lspconfig_util.add_hook_after(lspconfig_util.on_setup, function(config, user_config)
config.on_new_config = lspconfig_util.add_hook_after(config.on_new_config, require('devcontainers').on_new_config)
end)This will add devcontainers.on_new_config hook to all LSP configurations.
The hook checks if .devcontainers directory exists in root_dir. If it does then the devcontainer is
started and then the server is spawned inside the container.
The devcontainers.on_new_config hook should generally be the last hook added (use lspconfig_util.add_hook_after)
because it modifies config.cmd replacing it with functions that do the path mapping between host/container,
so modifying config.cmd after this is not possible.
Alternatively you can perform the setup only for selected LSP clients, e.g.
require('lspconfig').clangd.setup {
cmd = { 'clangd', '--background-index', '--clang-tidy' },
on_new_config = require('devcontainers').on_new_config,
}If the setup is correct, this plugin does not require anything more - the LSP should just work. When you open a file for which LSP has been configured, the devcontainer will be started and then LSP client will connect and use the LSP server inside the container.
Some additional commands are available:
DevcontainersUp- start devcontainerDevcontainersExec cmd...- execute a command inside devcontainer and print the result