helix
helix copied to clipboard
Allow remapping rootPath and rootUri sent to an LSP server to support running LSP inside containers
I am attempting to get a dockerized LSP server running for a PHP project with Psalm. I am able to correctly set the language server path through the local dev container orchestrator (ddev), thanks to the addition of per-directory language configuration support. Here is my example workspace specific configuration:
[[language]]
name = "php"
## This invocation is correct and attaches
language-server = { command = "ddev", args = ["exec", "php", "vendor/bin/psalm-language-server"] }
Under the hood, this is using the -i switch to attach the stdout of the container to the caller (helix in this case). I am able to see the initialization succeed in the log, and error messages when attempting to use gd which is implemented by this LSP. The error is essentially that it is trying to access the file by the host's path, but can't find it because it runs in the container's file namespace.
I attempted setting the rootPath and rootUri in the [language.config] section but this does not affect the rootPath and rootUri sent in the initialization, and is ignored by this language server in the didChangeConfiguration event and in the intializationOptions passed. Thus, I need to be able to pass a map such as this, and have the lsp intialization logic in helix apply it to the rootpath and rootUri in the initialization call.
Something like this:
[[language]]
name = "php"
language-server = { command = "ddev", args = ["exec", "php", "vendor/bin/psalm-language-server"] }
path-map = "/home/epocsquadron/path/to/dir:/var/www/html"
Then in the rootPath and rootUri there would be a replacement of the first part with the second, delimited by the semicolon.
I ran into this as well. I don't even need a path map, as for every project of this nature, the root URI never changes, and I would merely set up something in the repository directory like .helix/languages.toml:
[[language]]
name = "elixir"
language-server = { command = "docker", args = ["exec", "-i", "dev_project", "/foo/bar/elixir-ls/release/language_server.sh"] }
root-uri = "/app/project"
And that would be sufficient.
Edit: After modifying helix because that seemed simple, I realized that is not adequate. It would have to convert all filenames it operates on as well, so it really would have to be a map from one directory to another.
I'm not super familiar with the internals of LSP but I think that the server needs access to the files in the project in order to be able to do things like finding definitions and such? (I'm taking inspiration on how they do it at the lspcontainers.nvim plugin).
With that in mind, I have created a script that invokes the LSP in a container while mounting the working directory. It looks like this (removed most of the case block to simplify the example:
#!/usr/bin/env bash
lsp=${1}
user="$(id -u)"
group="$(id -g)"
case ${lsp} in
bash)
docker run -ti -v ${PWD}:${PWD} --workdir ${PWD} --user ${user}:${group} lspcontainers/bash-language-server
;;
esac
I then have this configuration in my languages.toml:
[[language]]
name = "bash"
language-server = { command = "/path/to/lsp.sh", args = ["bash"] }
When opening a bash script, this is what I see on both the helix logs and the container logs:
# helix log
2023-03-18T15:33:03.161 helix_lsp::transport [ERROR] err: <- StreamClosed
# container logs
Content-Length: 419
{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"textDocumentSync":1,"completionProvider":{"resolveProvider":true,"triggerCharacters":["$","{"]},"hoverProvider":true,"documentHighlightProvider":true,"definitionProvider":true,"documentSymbolProvider":true,"workspaceSymbolProvider":true,"referencesProvider":true,"codeActionProvider":{"codeActionKinds":["quickfix"],"resolveProvider":false,"workDoneProgress":false}}}}Content-Length: 102
{"jsonrpc":"2.0","id":0,"method":"workspace/configuration","params":{"items":[{"section":"bashIde"}]}}Content-Length: 227
{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"14:33:00.005 INFO BackgroundAnalysis: resolving glob \"**/*@(.sh|.inc|.bash|.command)\" inside \"file:///redacted/path\"..."}}Content-Length: 163
{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"14:33:00.033 INFO BackgroundAnalysis: Glob resolved with 6 files after 0.026 seconds"}}Content-Length: 146
{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":3,"message":"14:33:00.046 INFO BackgroundAnalysis: Completed after 0.04 seconds."}}Content-Length: 184
{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":2,"message":"14:33:00.525 WARNING ⛔️ ShellCheck: disabling linting as no executable was found at path 'shellcheck'"}}Content-Length: 170
{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///redacted/path/scripts/lsp.sh","version":0,"diagnostics":[]}}
So the nice thing is that helix is able to start the container and send some information to the server. The sad thing is that the server seems to die. I have also tried other containers (typescript and terraform) and I observe the same behavior.
I even went on to create my own container, fearing that maybe there was something odd about the lspcontainers container but I observed the same behavior there.
So I wonder what is happening behind the scenes that is making helix kill the container or why is the container deciding to quit after that first interaction with helix.
The last thing that I tried was also to not run a custom script and instead just call the container straight from the languages.tomlfile:
[[language]]
name = "bash"
language-server = { command = "docker", args = ["run", "-i", "lspcontainers/bash-language-server"] }
Bu that gives me the same results. :(
The reason why I want to keep going in this direction is because my configuration files would become very portable (jumping from one dev machine to another) and also keeps the file system clean. And if we can get this working as it is, then maybe we don't need to modify helix internally to make it work with containers.
If anyone can point me in the right direction to enable more debugging flags I'd be happy to keep on trying.
Yes, the language server does need to have access to the files. In my original post I am using ddev, which takes care of this behind the scenes. I wouldn't want to commit Helix to a particular way of seeing up containers, as the are a few container tools out there that one may want to piggyback onto, like devcontainers. Path mapping is I think a more portable solution.
In your tests, are you starting the container from the lsp command itself? It may be helix doesn't keep the subshell open after starting the command somehow. Could you try spawning the container outside of helix then attaching to it from the lsp command instead? This may end up deserving it's own issue separate of this one.
Yes, the language server does need to have access to the files.
Ok, thanks for the clarification.
In your tests, are you starting the container from the lsp command itself?
I'm not starting the container directly. It's helix that's spinning up the container for me.
Could you try spawning the container outside of helix then attaching to it from the lsp command instead?
I can give it a go but it I might shed some tears. I'll report back.
This may end up deserving it's own issue separate of this one.
I agree. Let me try the spawning + attaching of the container first. Then I can open up a new issue. Sorry for hijacking!
EDIT 1: styles