Skip to content

Generate requests/responses/notifications#31

Merged
rchl merged 3 commits intomainfrom
feat/requests
Feb 26, 2026
Merged

Generate requests/responses/notifications#31
rchl merged 3 commits intomainfrom
feat/requests

Conversation

@rchl
Copy link
Member

@rchl rchl commented Feb 26, 2026

I've figured out a nice way to make code like https://github.com/sublimelsp/LSP-basedpyright/blob/941722d6b075997beca3b183d4b6ef078c3d2784/plugin/client.py#L104-L127 nice and type safe without casts.

We will just pass a ServerResponse parameter and type checker will automatically infer type of result after if method == 'foo/bar'.

I don't see a way we could use those extra types for our Request/Response/Notification classes but it will at least allow making LspPlugin.on_server_response_async, LspPlugin.on_pre_send_request_async and LspPlugin.on_pre_send_notification_async nice and type safe for the new plugin API.

@predragnikolic @jwortmann

"""The declaration of a symbol representation as one or many {@link Location locations}."""

DeclarationLink = 'LocationLink'
DeclarationLink: TypeAlias = 'LocationLink'
Copy link
Member Author

@rchl rchl Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeAlias was needed for ambiguous cases like this one where this is otherwise treated as a variable assignment.

@predragnikolic
Copy link
Member

There is an experiment branch in here where I experimented with generating requests.
the generated file is:
https://github.com/sublimelsp/lsp-python-types/blob/experiment/lsp_requests.py

Here is an example of how it can be used:
https://github.com/Istok-Mir/Mir/blob/main/code_actions_on_save.py#L47-L59

Note when doing this I did conclude that I do not need to generate notifications, although it is possible.

@rchl
Copy link
Member Author

rchl commented Feb 26, 2026

Yeah, but it's a different use case really. As I've said in my comment, this is not to be used by our send_request/send_notification functionality.

@rchl
Copy link
Member Author

rchl commented Feb 26, 2026

There is an experiment branch in here where I experimented with generating requests. the generated file is: https://github.com/sublimelsp/lsp-python-types/blob/experiment/lsp_requests.py

Here is an example of how it can be used: https://github.com/Istok-Mir/Mir/blob/main/code_actions_on_save.py#L47-L59

Note when doing this I did conclude that I do not need to generate notifications, although it is possible.

Something like this could get introduced separately but I'm affraid that while it would add some automation and type safety, we would then potentially have to create wrappers for some of those because we might call some from different places and it sometimes takes quite a bit of code to prepare the params. Now we have our custom code and can put the logic there.

messageDirection: MessageDirection
method: str
params: NotRequired[EveryType | list[EveryType]]
params: NotRequired[EveryType]
Copy link
Member

@predragnikolic predragnikolic Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the previous type did match the JSON schema - so the previous type was more correct.

    "Notification": {
        ...
        "params": {
          "anyOf": [
            {
              "$ref": "#/definitions/Type"
            },
            {
              "items": {
                "$ref": "#/definitions/Type"
              },
              "type": "array"
            }
          ],
          "description": "The parameter type(s) if any."
        },

Copy link
Member Author

@rchl rchl Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example params is not a list. It's still an object.

The original type is not correct and also triggers a type error when passed to format_type.

Copy link
Member

@predragnikolic predragnikolic Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. we have a JSON schema lsprotocol/lsp.schema.json

  2. that JSON schema describes the data shape in lsprotocol/lsp.json

  3. The JSON schema for the Notification params key, describe the type as:

"params": {
          "anyOf": [
            {
              "$ref": "#/definitions/Type"
            },
            {
              "items": {
                "$ref": "#/definitions/Type"
              },
              "type": "array"
            }
          ],

To translated that to python params: Type | list[Type]
and it was marked as not required, as it was not listed as required in the JSON schema -

so the python type should be param: NotRequired[Type | list[Type]]

  1. The lsp_schema.py offers python types that are a 1:1 match to the lsprotocol/lsp.schema.json - if the python type doesn't align with the JSON schema that is a bug

  2. That was the reason why params was typed as params: NotRequired[EveryType | list[EveryType]]

Copy link
Member

@predragnikolic predragnikolic Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example params is not a list. It's still an object.

Can you explain more -> "It's still an object." my eyes don't see an object :)
my eyes see params, as params: Type | list[Type] (or more precisely params: NotRequired[EveryType | list[EveryType]] if I take into account that params is not required)

The original type is not correct and also triggers a type error when passed to format_type.

Can you provide an example (screenshot)? I can take a look. (I could not see anything wrong)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I will look into the type error it causes and address it differently.

Copy link
Member Author

@rchl rchl Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think the json schema is "wrong".

Text specification states that Request params are:

params?: array | object

so it kinda makes sense what JSON schema came up with:

        "params": {
          "anyOf": [
            {
              "$ref": "#/definitions/Type"
            },
            {
              "items": {
                "$ref": "#/definitions/Type"
              },
              "type": "array"
            }
          ],
          "description": "The parameter type(s) if any."
        },

but really what is:

            {
              "items": {
                "$ref": "#/definitions/Type"
              },
              "type": "array"
            }

if not just the #/definitions/Type itself which already includes the #/definitions/ArrayType type:

    "ArrayType": {
      "additionalProperties": false,
      "description": "Represents an array type (e.g. `TextDocument[]`).",
      "properties": {
        "element": {
          "$ref": "#/definitions/Type"
        },
        "kind": {
          "const": "array",
          "type": "string"
        }
      },
      "required": [
        "kind",
        "element"
      ],
      "type": "object"
    },

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And note that none of the params or result in https://github.com/sublimelsp/lsp-python-types/blob/main/lsprotocol/lsp.json have params: [...]. It would have to be that to match list[EveryType].

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could change it back to the original type, allow that type in format_type and then not handle that case since it's clearly not meant as a actual case with the schema file. But that would be just a different workaround with more code.

messageDirection: MessageDirection
method: str
params: NotRequired[EveryType | list[EveryType]]
params: NotRequired[EveryType]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the previous type was more correct

"params": {
"anyOf": [
{
"$ref": "#/definitions/Type"
},
{
"items": {
"$ref": "#/definitions/Type"
},
"type": "array"
}
],

    "Request": {
        "params": {
          "anyOf": [
            {
              "$ref": "#/definitions/Type"
            },
            {
              "items": {
                "$ref": "#/definitions/Type"
              },
              "type": "array"
            }
          ],
          "description": "The parameter type(s) if any."
        },

I would suggest reverting this.

Copy link
Member

@predragnikolic predragnikolic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this change ok. I misunderstood the use-case at first.

There is only one suggestion to revert the type change.

@rchl rchl merged commit 652036a into main Feb 26, 2026
1 check passed
@rchl rchl deleted the feat/requests branch February 26, 2026 22:14
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

Successfully merging this pull request may close these issues.

2 participants