Skip to main content

LSP/JSONRPC multiplexer for connecting one LSP client to multiple servers

Project description

Tests PyPI version

rassumfrassum

Connects one LSP client to multiple LSP servers.

It spawns one or more stdio-enabled LSP server subprocesses, communicates with them via pipes, and handles a client connected to its own stdio. rass behaves like an LSP server, so clients think they are talking to single LSP server, even though they are secretly talking to many.

demo

An LSP client like Emacs's Eglot can find a python file in some project and invoke it like so (C-u M-x eglot probably helps):

rass -- basedpyright-langserver --stdio -- ruff server

This should start managing Python files within a project with two servers instead of one. The -- separate rass's options from basedpyright's from ruff's.

To set up other clients, check their documentation.

Issues?

Read this first, please.

Installation

I hope to have made pip install rassumfrassum do the right thing by now. If I haven't, you can probably clone this repo and call the top-level rass wrapper script directly, since this doesn't have any dependencies.

Features

  • Merges and synchronizes diagnostics from multiple servers into a single textDocument/publishDiagnostics event.
  • Requests textDocument/codeActions from all servers supporting it; other requests go to the first server that supports the corresponding capability.
  • Tries its best to merge server capabilities announcements and to track which inferior server supports which capability.
  • Zero dependencies beyond Python standard library (3.10+)

Under the hood

Message Routing

JSONRPC has requests, responses, and notifications. Here's how they're routed:

From client to servers:

  • All notifications go unchanged directly to all servers

  • Some requests go only to one server, and that server's response is forwarded to the client

  • Other requests go to multiple servers, and their responses are merged if they arrive in time

From servers to client:

  • Most notifications go directly through, but some like textDocument/publishDiagnostics wait for all servers to send theirs, then the results are merged before forwarding to the client

  • 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.py is the main entry point with command-line processing and argument parsing. It calls run_multiplexer from rassum.py to start the multiplexer.

  • rassum.py contains run_multiplexer which starts a bunch of async tasks to read from the clients and servers, and waits for all of them. The local lexical state in run_multiplexer tracks JSONRPC requests, responses, and notifications, and crucially the progress of ongoing aggregation attempts. In as much as possible, rassum.py should 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 in frassum.py about in-transit messages.

  • frassum.py contains the business logic used by rassum.py facilities. This one fully knows about LSP. So it knows, for example, how to merge initialize and shutdown responses, when to reject a stale textDocument/publishDiagnostics and how to do the actual work for aggregation.

  • util.py provides logging utilities and general-purpose helpers like dict merging for debugging and monitoring the multiplexer's operation.

  • test.py contains test utilities used by both client and server test scripts.

  • json.py handles 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][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?

It would be nice to have "presets", each preset being a <languagename>.toml file with directives on which servers to run with what options and maybe a custom logic class to go with it. Then you could just type rass python to launch basedpyright+ruff or rass js to run typescript-language-server+eslint, etc.

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. This is untested and probably overkill for now, might be useful in the future.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rassumfrassum-0.1.5.tar.gz (33.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rassumfrassum-0.1.5-py3-none-any.whl (32.3 kB view details)

Uploaded Python 3

File details

Details for the file rassumfrassum-0.1.5.tar.gz.

File metadata

  • Download URL: rassumfrassum-0.1.5.tar.gz
  • Upload date:
  • Size: 33.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for rassumfrassum-0.1.5.tar.gz
Algorithm Hash digest
SHA256 2491af51cd80abde145c56d6ab547b155216e5379805821c6f42fb01d44f9e2d
MD5 8f1b0bbca49892162dc34d9fe0b11501
BLAKE2b-256 07b5ba068a3fa188ee616ca93286e6f1169fc9d72823b17dd177d175759836ff

See more details on using hashes here.

Provenance

The following attestation bundles were made for rassumfrassum-0.1.5.tar.gz:

Publisher: publish.yml on joaotavora/rassumfrassum

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rassumfrassum-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: rassumfrassum-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 32.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for rassumfrassum-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 5a0f418bab6d9151c27e6abd8985d4626766ba5ef7c57d4b1b8c4cf25c6181ce
MD5 fe78657fc70120c4024b14f299ac6a98
BLAKE2b-256 1920677201be06170baeabfc5c2293e0b0a4c96827adf220d932aade03b24e55

See more details on using hashes here.

Provenance

The following attestation bundles were made for rassumfrassum-0.1.5-py3-none-any.whl:

Publisher: publish.yml on joaotavora/rassumfrassum

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page