openlawlibrary/pygls

Dynamically register server capabilities in `initialize`

Opened this issue ยท 3 comments

Hey there ๐Ÿ‘‹

I'm interested in dynamically registering the features that our LSP server supports, based on client configuration. Concretely, while the Ruff LSP has traditionally surfaced lint diagnostics, it can now also support code formatting, and I'd like users to be able to "turn off" the formatting and/or linting behaviors as desired, based on client configuration.

The way we solve this today is via an environment variable. The VS Code extension registers a setting in VS Code, and then the client sets an environment variable prior to starting the server. The server then reads that variable and conditionally registers the text formatting action:

if RUFF_EXPERIMENTAL_FORMATTER:

    @LSP_SERVER.feature(TEXT_DOCUMENT_FORMATTING)
    async def format_document(
        ls: server.LanguageServer,
        params: DocumentFormattingParams,
    ) -> list[TextEdit] | None:
        return await _format_document_impl(ls, params)

This is fine, but it won't work as well for other LSP clients. It'd be nice if this were instead driven by a configuration option that was passed from client to server...

My thinking was to instead move this into the @LSP_SERVER.feature(INITIALIZE) handler: so, look at the provided settings, and register functions conditionally, something like:

@LSP_SERVER.feature(INITIALIZE)
def initialize(params: InitializeParams) -> None:
    """LSP handler for initialize request."""

    if params.initialization_options.get("settings", {}).get("format"):

        @LSP_SERVER.feature(TEXT_DOCUMENT_FORMATTING)
        async def format_document(
            ls: server.LanguageServer,
            params: DocumentFormattingParams,
        ) -> list[TextEdit] | None:
            return await _format_document_impl(ls, params)

    ...

(IIUC, this would require that the user restart the LSP when changing configuration, but that's fine.)

However, in testing, while the registration is being called when I'd expect it to, VS Code is saying that the capabilities aren't being registered:

Screen Shot 2023-09-26 at 11 25 59 PM

Is this something that you would expect to work? Is there a better way to accomplish this client-driven dynamic server registration?

Thank you in advance, grateful for all the work you do on pygls!

Is this something that you would expect to work?

TL;DR: No, (not with current pygls at least) but dynamic registration might be what you are looking for.

The @server.feature() decorator only implements "static registration" where the server publishes all the features it supports as part of the INITIALIZE handshake. By the time pygls calls your INITIALIZE handler, it has already computed the response it's going to send to the client and so any features registered in your handler will be missed.

While it would be interesting to try add some mechanism to support this in the future - it's not going to help you today.

I haven't used them yet myself but, pygls does support the client/registerCapability and client/unregisterCapability methods, we don't do the best job of advertising it (see #376 for a discussion on that). The json_server.py server has an example showing how to register (and unregister) a feature dynamically.

Some other notes on dynamic registration

  • As per the spec, the register/unregister methods can't be called in INITIALIZE, but calling them from an INITIALIZED handler should work.
  • It won't work for all LSP clients as dynamic registration support is optional, but you can check the InitializeParams.capabilities object to determine this.

Hope that helps!

This is great, thank you so much @alcarney. Feel free to close (or leave open, totally up to you).

P.S. I think the link to json_server.py is pointing to this issue rather than the file, which is a mistake I make all the time on my own projects...

I think the link to json_server.py is pointing to this issue rather than the file

Woops! ๐Ÿคฆ Should be fixed now!

I'll leave this open, as I think we should probably enable something along the lines of your original attempt so that it's possible to support clients that don't implement dynamic registration :)