-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Running language servers in containers
Say you are developing code on a local install of a newer Linux distribution to get all the features offered there, but the code you are developing is intended to be run on a more legacy system. The libraries it needs to run, etc. all won't work on your host system, nor do you want them there, so you actually run and build the code inside of a Docker container. The process of getting each language server to work might be a bit nuanced, but this page will help you get started and help bring to light some troubles that might occur along the way.
require('lspconfig')['pylsp'].setup {
before_init = function(params)
params.processId = vim.NIL
end,
cmd = {
'docker',
'run',
'-i',
'--rm',
'-v',
'/local/path/to/project:/container/path/to/project',
'your_project_image:your_project_tag',
'pylsp'
},
...
}
clangd is a C family language server, as the name implies. In order to get this working inside of a container, while Neovim runs on the host system, the setup might look something like the following:
cclangd: A shell script wrapper to run clangd
inside of the passed container, or normally if no matching container is found
#! /bin/sh
# The name of the container to run `clangd` in must be passed as the first and only argument
#
# This is based off the name of the buffer and the repository, etc. it is in, so even if we
# don't end up attaching to a container, it will still be passed
[ "$#" -ne 1 ] && echo "Container name required as first and only argument" >&2 && exit 1
# Verify that a contianer by this name actually exists, and is running
if [ -z "$(docker ps -q -f name=$1 -f status=running)" ]; then
clangd --background-index
else
# Important part here is both the '-i' and the redirection of STDERR
docker exec -i "$1" /usr/bin/clangd --background-index 2>/dev/null
fi
Then make sure that the above script is marked as executable, and in a runnable location on your system. On Linux, this mean that it lies somewhere on your $PATH
, and has bee altered with chmod +x /path/to/cclangd
. Then for your own client configuration via Neovim:
-- Notably _not_ including `compile_commands.json`, as we want the entire project
local root_pattern = lspconfig.util.root_pattern('.git')
-- Might be cleaner to try to expose this as a pattern from `lspconfig.util`, as
-- really it is just stolen from part of the `clangd` config
local function project_name_to_container_name()
-- Turn the name of the current file into the name of an expected container, assuming that
-- the container running/building this file is named the same as the basename of the project
-- that the file is in
--
-- The name of the current buffer
local bufname = vim.api.nvim_buf_get_name(0)
-- Turned into a filename
local filename = lspconfig.util.path.is_absolute(bufname) and bufname or lspconfig.util.path.join(vim.loop.cwd(), bufname)
-- Then the directory of the project
local project_dirname = root_pattern(filename) or lspconfig.util.path.dirname(filename)
-- And finally perform what is essentially a `basename` on this directory
return vim.fn.fnamemodify(lspconfig.util.find_git_ancestor(project_dirname), ':t')
end
-- Note that via the `manager` from `server_per_root_dir_manager`, we'll get a separate instance
-- of `clangd` as we switch between files, or even projects, inside of the right container
--
-- Finally, we've formed the "basename of a project" to pass to our `cclangd` script, which will
-- then look for a matching container, or run `clangd` normally if no matching container is found
-- /path/to/my/project
-- would look for a container named `project`, and `docker exec` a `clangd` instance there, etc.
lspconfig.clangd.setup{
cmd = {
'cclangd',
project_name_to_container_name(),
},
}
in addition to anything else you've already setup, like a custom on_attach
function, etc. although the configuration additions above are all that is required.
- If you have a project that is setup in such a way that header files aren't installed to their standard locations on your system, each binary requires customized linking, etc. I'd highly recommend playing around with either compiledb or Bear. Both of these will generate
compile_commands.json
files, which are already recognized by the defaultclangd
configuration in this repo. - You can edit the
cclangd
script to redirectSTDERR
to a file if you are interested in debugging things, just change... 2>/dev/null
to whatever filepath you like;... 2>/tmp/clangd.log
, etc. - The builtin client and the handling in this repo. do a great job of handling root paths and whatnot, but you might have to play around with the
-w
option of docker exec depending on how your project is setup.
The vscode-languageserver-node
based server expects a client to create it. The server expects the client to supply a process id (the process id of the client). When the server cannot detect the process id, it assumes the client has exited and that it should exit. Since containers do not share process ids with the host this results in the server exiting immediately. See here
A way to handle this is using before_init
and overriding initialize_params.process_id
with a NIL (null) value as described here
A less ideal way to deal with this is to have the host and container share process ids via the docker/podman run argument --pid=host
as mentioned here. Do note this is considered an insecure practice by both Docker and Podman. man docker-run
or man podman-run
and /--pid=
for more info.