Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lsp_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@
CompletionRequest,
ContentChange,
DefinitionRequest,
ErrorCodes,
ExitNotification,
HoverRequest,
InitializeParams,
InitializeRequest,
InitializedNotification,
Message,
NotificationMessage,
Position,
ProgressNotification,
ProgressParams,
Range,
RequestMessage,
ResponseError,
ResponseMessage,
ShutdownRequest,
TextDocumentDidChangeNotification,
TextDocumentDidCloseNotification,
Expand Down Expand Up @@ -51,14 +57,20 @@
"CompletionRequest",
"ContentChange",
"DefinitionRequest",
"ErrorCodes",
"ExitNotification",
"HoverRequest",
"InitializeParams",
"InitializeRequest",
"InitializedNotification",
"LSPClient",
"Message",
"NotificationMessage",
"ProgressNotification",
"ProgressParams",
"RequestMessage",
"ResponseError",
"ResponseMessage",
"ShutdownRequest",
"TextDocumentDidChangeNotification",
"TextDocumentDidCloseNotification",
Expand Down
75 changes: 71 additions & 4 deletions lsp_client/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,89 @@
for reference, and what a correct and complete implementation should look like.
"""

from enum import IntEnum
from typing import Any, List, Literal, Optional

from pydantic import BaseModel, Field


class BaseNotification(BaseModel):
"""LSP notification — like a request but without an id field."""
# Base Protocol — abstract Message
# See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#abstractMessage # noqa: E501


class Message(BaseModel):
"""Base of every LSP message; carries only the JSON-RPC version."""

jsonrpc: str = Field(default="2.0")


class BaseNotification(Message):
"""LSP notification — like a request but without an id field."""

method: str
params: dict | None = Field(default=None)


class BaseRequest(BaseModel):
jsonrpc: str = Field(default="2.0")
class BaseRequest(Message):
id: int | None = Field(default=None)
method: str
params: dict | None = Field(default=None)


class ResponseError(BaseModel):
"""The error object returned on a failed request."""

code: int
message: str
data: Any | None = None


class ResponseMessage(Message):
"""A response to a request.

``id`` may be ``null`` when the request id could not be determined (e.g. a
parse error). Exactly one of ``result`` / ``error`` is present per the spec.
"""

id: int | str | None = None
result: Any | None = None
error: ResponseError | None = None


class ErrorCodes(IntEnum):
"""JSON-RPC and LSP-defined error codes.

Codes that share a value (e.g. ``serverErrorStart`` /
``jsonrpcReservedErrorRangeStart``) resolve to enum aliases.
"""

# Defined by JSON-RPC
ParseError = -32700
InvalidRequest = -32600
MethodNotFound = -32601
InvalidParams = -32602
InternalError = -32603

jsonrpcReservedErrorRangeStart = -32099
serverErrorStart = -32099

ServerNotInitialized = -32002
UnknownErrorCode = -32001

jsonrpcReservedErrorRangeEnd = -32000
serverErrorEnd = -32000

# Defined by LSP
lspReservedErrorRangeStart = -32899

RequestFailed = -32803
ServerCancelled = -32802
ContentModified = -32801
RequestCancelled = -32800

lspReservedErrorRangeEnd = -32800


class ProtocolError(Exception):
def __init__(self, message: str, errors: Optional[List[str]] = None) -> None:
"""
Expand Down Expand Up @@ -348,6 +411,10 @@ def __init__(self, **kwargs: Any) -> None:
super(WorkDoneProgressCancelNotification, self).__init__(**kwargs)


# Spec-named aliases for the base protocol message types.
RequestMessage = BaseRequest
NotificationMessage = BaseNotification

# Backwards-compatible aliases for renamed classes
TextDocumentDidOpenRequest = TextDocumentDidOpenNotification
TextDocumentDidChangeRequest = TextDocumentDidChangeNotification
Expand Down
59 changes: 59 additions & 0 deletions tests/test_protocol.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
from lsp_client.protocol import (
BaseNotification,
BaseRequest,
CancelRequest,
ClientInfo,
CompletionRequest,
ContentChange,
DefinitionRequest,
ErrorCodes,
ExitNotification,
HoverRequest,
InitializeParams,
InitializeRequest,
InitializedNotification,
Message,
NotificationMessage,
Position,
ProgressNotification,
Range,
RequestMessage,
ResponseError,
ResponseMessage,
ShutdownRequest,
TextDocumentDidChangeNotification,
TextDocumentDidCloseNotification,
Expand Down Expand Up @@ -206,3 +214,54 @@ def test_work_done_progress_cancel_notification():
assert data["method"] == "window/workDoneProgress/cancel"
assert "id" not in data
assert data["params"] == {"token": 42}


def test_message_base_defaults():
message = Message()
assert message.jsonrpc == "2.0"
assert message.model_dump() == {"jsonrpc": "2.0"}


def test_requests_and_notifications_are_messages():
# The base protocol abstract Message is the root of every message type.
assert issubclass(BaseRequest, Message)
assert issubclass(BaseNotification, Message)
assert RequestMessage is BaseRequest
assert NotificationMessage is BaseNotification


def test_response_message_success():
response = ResponseMessage(id=1, result={"capabilities": {}})
data = response.model_dump(exclude_none=True)
assert data == {"jsonrpc": "2.0", "id": 1, "result": {"capabilities": {}}}
assert "error" not in data


def test_response_message_error():
response = ResponseMessage(
id=1,
error=ResponseError(code=ErrorCodes.MethodNotFound, message="no such method"),
)
data = response.model_dump(exclude_none=True)
assert data["error"] == {"code": -32601, "message": "no such method"}
assert "result" not in data


def test_response_message_null_id():
# id may be null when it cannot be determined (e.g. a parse error).
response = ResponseMessage(
id=None,
error=ResponseError(code=ErrorCodes.ParseError, message="parse error"),
)
data = response.model_dump()
assert data["id"] is None


def test_error_codes_values():
assert ErrorCodes.ParseError == -32700
assert ErrorCodes.InvalidRequest == -32600
assert ErrorCodes.InternalError == -32603
assert ErrorCodes.RequestCancelled == -32800
assert ErrorCodes.ContentModified == -32801
# Shared-value codes resolve to aliases of the canonical member.
assert ErrorCodes.serverErrorStart is ErrorCodes.jsonrpcReservedErrorRangeStart
Loading