emacs-lsp-booster
emacs-lsp-booster copied to clipboard
Idea: wrap multiple servers
Some servers are intended to be "add-ons", providing only certain features and expecting other servers to provide the rest. A classic combination is e.g. pyright + ruff-lsp. Juggling multiple servers is possible in many editors, and in Emacs with LSP (but not eglot). But all of the process contention and JSON performance issues that motivated this package are compounded when communicating with multiple LSP servers at the same time.
eglot-lsp-booster is well poised to help with this I think.
Imagine a call like:
emacs-lsp-booster --multiserver --json-false-value :json-false -- -c server1.cfg /path/to/server1 --server1-flags -- -c server2.cfg /path/to/server2 --server2-flags
With a call like this, emacs-lsp-booster would start and communicate with two different servers at the same time, caching and translating the requests to and responses from both.
Here server1.cfg and server2.cfg would be configuration files that instruct emacs-lsp-booster how set the priority for request types (these could also be passed as arguments). There would seem to be a number of ways to do this, but one simple idea:
- If
server1.cfgmentions a given request name, send those requests to it. - If it does not, but
server2.cfgmentions it, send those requests to server2. - If neither server config mentions it, send to both.
- Forward all responses from both servers to the client.
One thing to be worked out is capability registration during initialize, i.e. when the servers respond with their capabilities, like:
(:id 1 :jsonrpc "2.0" :result
(:capabilities...
It might be nice if emacs-lsp-booster could "spy" on this traffic, merging the capabilities to send back to the client, and saving an internal table of capabilities per-server to help direct requests to the correct server.
To be honest.
This feature would be a life savier, as it would make eglot step into the modern world of 'multiple lsps in the same buffer'.
I personally use from 2 to 4 lsps servers per buffer in most of my job projects. A feature like this could put Eglot in the game again 😄.
Edit: if anyone is willing to implement it, I am happy to test it!
When you use so many servers, how are you deciding and configuring which requests go to which servers?
Well, I might not be the best to answer this, as I am mostly an USER of LSPs.
I primarily use LSPs in Emacs, mostly with lsp-mode rather than eglot, but this applies generally to any editor that supports multiple LSP servers.
In my experience, LSP servers often complement each other rather than conflict, as each serves a distinct purpose. For example:
- Language Servers: Handle standard LSP features like go-to-definition, completion, and hover.
- Linters: Provide diagnostics (errors/warnings), which might overlap with the language server.
- Embedded DSL Servers: Support inline languages, such as SQL inside a JavaScript string.
- Formatters: Enforce consistent code formatting.
When hovering over code, the editor collects responses from all active LSPs and displays them merged, separated by a bar (when inline) or an horizontal bar when in a floating box. If multiple servers report the same diagnostic message (e.g., a linter and the language server flagging the same error), the messages are deduplicated.
For code actions, the options are presented with an indication of which server provided them, such as:
Do this [server 1]
Do that [server 1]
Do another thing [server 2]
This allows users to see which LSP is suggesting an action and choose accordingly.
I think that would be the general idea.
But all of the process contention and JSON performance issues that motivated this package are compounded when communicating with multiple LSP servers at the same time.
I don't think I fully understand this part. Could you please elaborate on this? I'm using lsp-mode, when multiple servers are used, each server is wrapped with its own emacs-lsp-booster, and AFAIK lsp-mode queries all servers fully in parallel asynchronously. I didn't observe any performance issues in this case.
Here server1.cfg and server2.cfg would be configuration files that instruct emacs-lsp-booster how set the priority for request types (these could also be passed as arguments). There would seem to be a number of ways to do this, but one simple idea:
So if I understand you correctly, what you mean is that emacs-lsp-booster can act as a "router" that forwards requests to different servers based on configurations and/or server capabilities, right?
However, I think for most of the requests, what we want is actually "merging results", instead of "select one" (e.g. code actions, or diagnostics). But that's a "application-level" thing, while emacs-lsp-booster more-like work in "transport-level" to me (like nginx proxy).
I don't think I fully understand this part. Could you please elaborate on this? I'm using lsp-mode, when multiple servers are used, each server is wrapped with its own emacs-lsp-booster, and AFAIK lsp-mode queries all servers fully in parallel asynchronously. I didn't observe any performance issues in this case.
I suppose I meant if you didn't wrap both of them separately, the JSON translation and I/O latency would be doubled-up. If you have the option to wrap both, that might work well, though I suspect it would always be better to offload server communication outside of emacs. Eglot does not offer any such option. So you can see the power in using an intermediary like emacs-lsp-booster to create "virtual servers" from two or more underlying ones.
So if I understand you correctly, what you mean is that emacs-lsp-booster can act as a "router" that forwards requests to different servers based on configurations and/or server capabilities, right?
Exactly. The details to figure out would be "which server(s) should I route this request to". After I wrote this I found this related discussion with eglot's author; he calls the idea a "server multiplexer". I'm proposing that emacs-lsp-booster is a good candidate for growing "server multiplex" capabilities. He's apparently playing with something in C++.
for most of the requests, what we want is actually "merging results"
It's an important point, and one which I'm not as clear on. I know many people composite servers together because they provide different capabilities, in which case merging responses isn't necessary. But yes, if two servers are combined both of which provide, say completion results, you might want to merge them (and de-duplicate candidates?). Or you can just send both sets of results and let the application side sort that out. I can imagine a completion at point function that merges asynchronously and "just in time", for example.
Update: this python server multiplexer does merge completions.
Update 2: Another typescript-based multiplexer has been created: lspx.
However, I think for most of the requests, what we want is actually "merging results", instead of "select one" (e.g. code actions, or diagnostics). But that's a "application-level" thing, while emacs-lsp-booster more-like work in "transport-level" to me (like nginx proxy).
While I agree that it is more of an application thing, I think given the performance issues with LSP this belongs nicely in something like lsp-booster. It's basically a new capability for eglot without any (at least very little) performance degradation.
@jdtsmith Both those multiplexers require bundled runtimes, which is unfortunate, as a user I would be much happier with just a small binary that does this than having to have some version of node or some version of python.
I agree. It seems @blahgeek already has a parallel servers solution working for them using lsp-mode, so not sure what the interest may be.
Any other rust developers interested in giving it a try? Could probably borrow some of the existing logic from lspx.
Here's another python multiplexer proxy. It has a pretty clear and well documented "division of labor" amongst the subordinate servers (specifying one as the "main server"). I think it would serve as a good template for emacs-lsp-booster, if there was interest in evolving in that direction.
Most other editors by now can handle multiple servers on their own, but since this proxy server is already emacs-specific, and emacs' builtin client eglot does not, this still seems like the obvious place to build this support. Sadly I don't have the time or Rust skills needed...
I am also working on a multiplexer solution mostly to explore the problem space (https://github.com/KristianAN/lspipe). I haven't had time/interest in working on it for the last months, but I will probably spend some more time with it in the upcoming months.
I'm still not 100 % sure if I agree with the sentiment that this shouldn't be a responsibility of emacs/eglot. Are there other motivations for working on this besides eglot?
Nice. My understanding is that most editors now support running multiple servers directly. E.g. Helix had a big debate, and considered a proxy of this type, but then implemented it. But your work would certainly have a big impact if it worked well with eglot. Does Haskell require a runtime or would it be a compiled wrapper?