-
-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Functionality for composing and routing to different LSP services #18
base: main
Are you sure you want to change the base?
Conversation
What's the use case of composing multiple LspService? Is it an LSP client dispatching requests to multiple servers? |
In my case the two
Basically I just wanted some events to be handled by one and some to be handled by the other, and the flexibility to seamlessly switch between them. Generally the utility of this |
I think this could be done currently using service layering, by passthrough uninterested messages? This only works for non-dynamic services and is asymmetric though. We do not support dynamic services yet, let alone dynamic and symmetric service composing. This patch is quite big.
This is an interesting benefit, but has a drawback of asking handlers one-by-one if they are interested. This is O(n) time complexity on each request or notification, comparing to the current single O(1) hash map lookup. |
That sounds quite a bit simpler, I'll try implementing something using this approach.
With |
I sketched a fallback fn emit(&mut self, event: AnyEvent) -> ControlFlow<Result<(), async_lsp::Error>> {
match self.primary.emit(event.clone()) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(Err(_)) => self.fallback.emit(event),
ControlFlow::Break(Ok(())) => ControlFlow::Break(Ok(())),
}
} Is that the sort of thing you had in mind? |
This is intended.
That's really an issue. Typically a middleware knows what kind of events they are interested in, so they inspect it by For your use case, I think the solution for now would be to intrusively make one of your service a middleware layer, handle interested requests, notifications and events, and pass through all others. A generic "composer service" would be hard and still need a decider like |
I hear you. The most useful and critical change in this whole PR is to make Actually I'm relying on this for my custom |
I went ahead and made my language server depend on a much more minimal set of changes:
I think these changes are pretty simple, and in my view they seem pretty essential for supporting downstream |
I wanted to compose two different
LspService
instances in my language server. I have a customLspService
implementation that routes messages to an actor as well as a trait that generates async streams from anLspRouter
. Tower has aSteer
utility that can be used to route messages to different services. I implemented asteer
module for async-lsp which features anLspSteer
service and anLspPicker
trait for specifying howLspSteer
should pick services.I also added a
CanHandle
trait which can be implemented byLspService
implementations to communicate about which messages they can field. The implementation forLspRouter
simply checks to see if there's a handler registered for a given message's key.Finally, I added a simple
FirstComeFirstServe
picker that usesCanHandle
methods to find the first service able to handle the message.Here's how I'm using this in my application: https://github.com/micahscopes/fe/blob/language-server-async/crates/language-server/src/server.rs#L58-L64
(If you're curious, the
streaming_router
is currently only used to do some chunked throttling of customNeedsDiagnostic
events that get emitted by various update-related handlers. It then emits another eventFilesNeedDiagnostics
which handles the actual diagnostics. I'm excited about the potential of combining streams with ordinaryLspRouter
handlers to get more precise control over timing and ordering of message handlers. That was the motivation for doing it this way.)By the way, this is an awesome library, thanks for building it!