Skip to content
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

Dynamically register server capabilities in initialize #381

Open
charliermarsh opened this issue Sep 27, 2023 · 3 comments
Open

Dynamically register server capabilities in initialize #381

charliermarsh opened this issue Sep 27, 2023 · 3 comments

Comments

@charliermarsh
Copy link

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!

@alcarney
Copy link
Collaborator

alcarney commented Sep 27, 2023

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!

@charliermarsh
Copy link
Author

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...

@alcarney
Copy link
Collaborator

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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants