LSP/JSONRPC multiplexer for connecting one LSP client to multiple servers
Project description
rassumfrassum
Connect an LSP client to multiple LSP servers.
The rass program, the main entry point, behaves like an LSP stdio
server, so clients think they are talking to single LSP server, even
though they are secretly talking to many. Behind the scenes more
stdio LSP server subprocesses are spawned.
Setup
Install the rass tool:
pip install rassumfrassum
Now install some language servers, say Python's basedpyright and ruff:
npm install -g basedpyright
pip install ruff
Tell your LSP client to call rass python:
-
In Emacs's Eglot, find a Python file in a project and
C-u M-x eglot RET rass python RET. -
In vanilla Neovim, use this snippet (briefly tested with
nvim --clean -u snippet.lua)
vim.lsp.config('rass-python', {
cmd = {'rass','python'},
filetypes = { 'python' },
root_markers = { '.git', },
})
vim.lsp.enable('rass-python')
Presets
Presets give you a uniform way to start typical sets of language
servers for a given language, while being flexible enough for
tweaking. Most presets would be Python files with a servers()
function that returns a list of server commands.
Advanced presets can hook into LSP messages to hide the typical initialization/configuration pains from clients, see vue.py.
Using Presets
The bundled python preset runs basedpyright and ruff:
rass python
You can add more servers on top of a preset using -- separators.
For example, to add codebook for spell checking:
rass python -- codebook-lsp server
User Presets
You can create your own presets or override bundled ones. Rass searches these locations in order:
$XDG_CONFIG_HOME/rassumfrassum/(if XDG_CONFIG_HOME is set)~/.config/rassumfrassum/(default)~/.rassumfrassum/(legacy)- Bundled presets directory (last resort)
To use ty instead of basedpyright, create ~/.config/rassumfrassum/python.py:
"""Python preset using ty instead of basedpyright."""
def servers():
return [
['ty', 'server'],
['ruff', 'server']
]
Issues?
Read this first, please.
Features
- Zero dependencies beyond Python standard library (3.10+)
Under the hood
- Tries its best to merge server capabilities announcements into a consistent aggregate capability set.
- Track which inferior server supports which capability.
- Merges and synchronizes diagnostics from multiple servers into a
single
textDocument/publishDiagnosticsevent. - Client requests for
textDocument/codeActionsandtextDocument/completionsgo to all servers supporting it, other requests go to the first server that supports the corresponding capability. - All server requests go to the client. ID tweaking is necessary because servers don't know about each other and they could clash.
Architecture
The codebase lives in src/rassumfrassum/ and is split into several modules:
-
main.pyis the main entry point with command-line processing and argument parsing. It callsrun_multiplexerfromrassum.pyto start the multiplexer. -
presets.pyhandles preset discovery and loading, searching user config directories (XDG-compliant) and bundled presets. -
rassum.pycontainsrun_multiplexerwhich starts a bunch of async tasks to read from the clients and servers, and waits for all of them. The local lexical state inrun_multiplexertracks JSONRPC requests, responses, and notifications, and crucially the progress of ongoing aggregation attempts. In as much as possible,rassum.pyshould be just a JSONRPC-aggregator and not know anything about particular custom handling of LSP message types. There are a few violations of this principle, but whenever it needs to know what to do, it asks/informs the upper layer infrassum.pyabout in-transit messages. -
frassum.pycontains the business logic used byrassum.pyfacilities. This one fully knows about LSP. So it knows, for example, how to mergeinitializeandshutdownresponses, when to reject a staletextDocument/publishDiagnosticsand how to do the actual work for aggregation. -
util.pyprovides logging utilities and general-purpose helpers like dict merging for debugging and monitoring the multiplexer's operation. -
test.pycontains test utilities used by both client and server test scripts. -
json.pyhandles bare JSON-over-stdio logistics and is completely ignorant of LSP. It deals with protocol framing and I/O operations.
Testing
There are tests under test/. Each test is a subdir, usually with a
client.py, a server.py (of which instances are spawned to emulate
multiple servers) and a run.sh, which creates a FIFO special file to
wire up the stdio connections and launches client.py connected to
rass. client.py has the test assertions. Both client.py and
server.py use common utils from src/rassumfrassum/test.py.
To run all tests, use test/run-all.sh.
Logging
The stderr output of rass is useful for peeking into the
conversation between all entities and understanding how the
multiplexer operates.
FAQ
(...not really, noone's really asked anything yet...)
Related projects?
There's lspx! Never tried it, but some people are using it. Development started in this Eglot discussion thread: https://github.com/joaotavora/eglot/discussions/1429
There's also this defunct lsplex thing by myself in C++ that went nowhere.
Project name?
I'm tired of fretting about names. Kudos if you can guess where I stole this one from. Used to be called dada, btw.
Bugs?
Probably a million. The LSP flora is hard enough to navigate, and maintaining the Eglot client is hard enough because of that. So this is fun and potentially useful but adds another failure point. A pretty big one at that, since of the hundreds (thousands?) of LSP servers out there, there are uncountable combinations of them, and some will definitely trip you up.
Issue reports?
Read the preceding section. If you use this and want to report
something, you can start discussions or create issues at will. If you
create an issue, I might just close it with a cantmakesenseofthis
label which just means I can't make sense of it just yet. Also I have
very little time for OSS these days, so this is a totally NO WARRANTY,
YMMV thing. If I close your issue just like that, doesn't mean you're
a bad person, so don't fret. If you can provide an easy, simple, 100%
idiot-proof recipe demonstrating the bug the chances that I'll address
it are slightly higher. Else, just fork this repo, this is just
Python and you're probably a programmer right?
Did I vibe code this junk?
Yeah, a bit, with some heavy coaching, then I took over. The boring bits are definitely an LLM's.
Future/roadmap?
I might rewrite this in Rust or C++ if it makes sense. Having an LSP middleware opens up some possibilities for making JSON communication more efficient.
Options to rass
Use --help to see all options.
The --delay-ms N option delays all JSONRPC messages sent to the
client by N milliseconds. Each message gets its own independent timer,
so if two messages arrive at t=0.5s and t=1.5s with a 3000ms
delay, they'll be dispatched at t=3.5s and t=4.5s
respectively. Useful for diagnostics and testing.
The --drop-tardy option controls an aspect of the "aggregation". If
it's true and a server takes too long to respond to a request, or send
a mergeworthy notification, any messages that arrive too late are
simply dropped and the client sees whatever it got when the timeout
expired. If it's false, the most up-to-date state of the aggregation
is simply retransmitted to the client. The default is false.
The --logic-class CLASS option specifies which routing logic class
to use. The default is LspLogic. You can specify a simple class
name (which will be looked up in the rassumfrassum.frassum module)
or a fully qualified class name like mymodule.MyCustomLogic. This
is useful for extending rass with custom routing behavior by
subclassing LspLogic.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rassumfrassum-0.2.2.tar.gz.
File metadata
- Download URL: rassumfrassum-0.2.2.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
152c133daf297f3074d8d11dcde8c11fafe0e3d9fd1eb7f02407189d3ce7e827
|
|
| MD5 |
b7b3ef1ae531ff8af170c4c59cffd5e6
|
|
| BLAKE2b-256 |
d8c94463b05d1090b701b6864d1ad95eaf9db5132b2e1f871d6ea91c5aaff551
|
Provenance
The following attestation bundles were made for rassumfrassum-0.2.2.tar.gz:
Publisher:
publish.yml on joaotavora/rassumfrassum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rassumfrassum-0.2.2.tar.gz -
Subject digest:
152c133daf297f3074d8d11dcde8c11fafe0e3d9fd1eb7f02407189d3ce7e827 - Sigstore transparency entry: 797072646
- Sigstore integration time:
-
Permalink:
joaotavora/rassumfrassum@7d562692bda29a4d90035762b1a601b24be04cf6 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/joaotavora
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7d562692bda29a4d90035762b1a601b24be04cf6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file rassumfrassum-0.2.2-py3-none-any.whl.
File metadata
- Download URL: rassumfrassum-0.2.2-py3-none-any.whl
- Upload date:
- Size: 40.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bfae9943d1843d527834bd55c8a41c2dc2047147daa21e019e0e487c2861763
|
|
| MD5 |
1bcc7c87b919b3b956e65e23ad6a9200
|
|
| BLAKE2b-256 |
dfe1bd79799fb4a5630baa62cf310125b184f5100c0171b5462a500f35b8050d
|
Provenance
The following attestation bundles were made for rassumfrassum-0.2.2-py3-none-any.whl:
Publisher:
publish.yml on joaotavora/rassumfrassum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rassumfrassum-0.2.2-py3-none-any.whl -
Subject digest:
7bfae9943d1843d527834bd55c8a41c2dc2047147daa21e019e0e487c2861763 - Sigstore transparency entry: 797072651
- Sigstore integration time:
-
Permalink:
joaotavora/rassumfrassum@7d562692bda29a4d90035762b1a601b24be04cf6 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/joaotavora
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7d562692bda29a4d90035762b1a601b24be04cf6 -
Trigger Event:
push
-
Statement type: