chore(lsprotocol): migrate to 2025.0.0 and cleanup artifacts
The changes align the project with the 2025.0.0 lsprotocol release, removing the old backport and updating type hints in the protocol hooks to use Sequence where appropriate. The dist-info and packaging metadata for older lsprotocol versions are replaced with the new 2025.0.0 artifacts. - Remove exceptiongroup backport used on Python <3.11 - Use Sequence instead of List in LS protocol hooks - Replace old dist-info with 2025.0.0 metadata
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from collections import namedtuple
|
||||
from typing import Any
|
||||
|
||||
from lsprotocol import converters
|
||||
|
||||
@@ -12,7 +11,6 @@ from pygls.protocol.json_rpc import (
|
||||
JsonRPCResponseMessage,
|
||||
)
|
||||
from pygls.protocol.language_server import LanguageServerProtocol, lsp_method
|
||||
from pygls.protocol.lsp_meta import LSPMeta, call_user_feature
|
||||
|
||||
|
||||
def _dict_to_object(d: Any):
|
||||
@@ -68,8 +66,6 @@ __all__ = (
|
||||
"JsonRPCRequestMessage",
|
||||
"JsonRPCResponseMessage",
|
||||
"JsonRPCNotification",
|
||||
"LSPMeta",
|
||||
"call_user_feature",
|
||||
"_dict_to_object",
|
||||
"_params_field_structure_hook",
|
||||
"_result_field_structure_hook",
|
||||
|
||||
@@ -15,54 +15,90 @@
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import enum
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import traceback
|
||||
import typing
|
||||
import uuid
|
||||
from concurrent.futures import Future
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pygls.server import LanguageServer, WebSocketTransportAdapter
|
||||
|
||||
from typing import Any, Callable, Protocol, Type, Union, runtime_checkable
|
||||
|
||||
import attrs
|
||||
from cattrs.errors import ClassValidationError
|
||||
|
||||
from lsprotocol.types import (
|
||||
CANCEL_REQUEST,
|
||||
EXIT,
|
||||
WORKSPACE_EXECUTE_COMMAND,
|
||||
ResponseError,
|
||||
ResponseErrorMessage,
|
||||
)
|
||||
|
||||
from pygls.exceptions import (
|
||||
FeatureNotificationError,
|
||||
FeatureRequestError,
|
||||
JsonRpcException,
|
||||
JsonRpcInternalError,
|
||||
JsonRpcInvalidParams,
|
||||
JsonRpcMethodNotFound,
|
||||
JsonRpcRequestCancelled,
|
||||
FeatureNotificationError,
|
||||
FeatureRequestError,
|
||||
)
|
||||
from pygls.feature_manager import FeatureManager, is_thread_function
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
from cattrs import Converter
|
||||
|
||||
from pygls.io_ import AsyncWriter, Writer
|
||||
from pygls.server import JsonRPCServer
|
||||
|
||||
MessageHandler = Union[Callable[[Any], Any],]
|
||||
MessageCallback = Callable[[Future[Any]], None]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# cattrs needs access to this type definition so we cannot include it in the
|
||||
# TYPE_CHECKING block above
|
||||
MsgId = Union[str, int]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCNotification(Protocol):
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCRequest(Protocol):
|
||||
id: MsgId
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCResponse(Protocol):
|
||||
id: MsgId
|
||||
jsonrpc: str
|
||||
result: Any
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCError(Protocol):
|
||||
id: MsgId
|
||||
jsonrpc: str
|
||||
error: Any
|
||||
|
||||
|
||||
RPCMessage = Union[RPCNotification, RPCResponse, RPCRequest, RPCError]
|
||||
|
||||
|
||||
@attrs.define
|
||||
class JsonRPCNotification:
|
||||
@@ -81,7 +117,7 @@ class JsonRPCRequestMessage:
|
||||
Used as a fallback for unknown types.
|
||||
"""
|
||||
|
||||
id: Union[int, str]
|
||||
id: MsgId
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
@@ -93,13 +129,13 @@ class JsonRPCResponseMessage:
|
||||
Used as a fallback for unknown types.
|
||||
"""
|
||||
|
||||
id: Union[int, str]
|
||||
id: MsgId
|
||||
jsonrpc: str
|
||||
result: Any
|
||||
|
||||
|
||||
class JsonRPCProtocol(asyncio.Protocol):
|
||||
"""Json RPC protocol implementation using on top of `asyncio.Protocol`.
|
||||
class JsonRPCProtocol:
|
||||
"""Json RPC protocol implementation
|
||||
|
||||
Specification of the protocol can be found here:
|
||||
https://www.jsonrpc.org/specification
|
||||
@@ -109,86 +145,156 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
CHARSET = "utf-8"
|
||||
CONTENT_TYPE = "application/vscode-jsonrpc"
|
||||
|
||||
MESSAGE_PATTERN = re.compile(
|
||||
rb"^(?:[^\r\n]+\r\n)*"
|
||||
+ rb"Content-Length: (?P<length>\d+)\r\n"
|
||||
+ rb"(?:[^\r\n]+\r\n)*\r\n"
|
||||
+ rb"(?P<body>{.*)",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
VERSION = "2.0"
|
||||
|
||||
def __init__(self, server: LanguageServer, converter):
|
||||
def __init__(self, server: JsonRPCServer, converter: Converter):
|
||||
self._server = server
|
||||
self._converter = converter
|
||||
|
||||
self._shutdown = False
|
||||
|
||||
# Book keeping for in-flight requests
|
||||
self._request_futures: Dict[str, Future[Any]] = {}
|
||||
self._result_types: Dict[str, Any] = {}
|
||||
self._ctx_msg_id: contextvars.ContextVar[MsgId | None] = contextvars.ContextVar(
|
||||
"msg_id", default=None
|
||||
)
|
||||
self._request_futures: dict[MsgId, Future[Any]] = {}
|
||||
self._result_types: dict[MsgId, Any] = {}
|
||||
|
||||
self.fm = FeatureManager(server, converter)
|
||||
self.transport: Optional[
|
||||
Union[asyncio.WriteTransport, WebSocketTransportAdapter]
|
||||
] = None
|
||||
self._message_buf: List[bytes] = []
|
||||
|
||||
self._send_only_body = False
|
||||
self.writer: AsyncWriter | Writer | None = None
|
||||
self._include_headers = False
|
||||
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
def _execute_notification(self, handler, *params):
|
||||
"""Executes notification message handler."""
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
future = asyncio.ensure_future(handler(*params))
|
||||
future.add_done_callback(self._execute_notification_callback)
|
||||
else:
|
||||
if is_thread_function(handler):
|
||||
self._server.thread_pool.apply_async(handler, (*params,))
|
||||
else:
|
||||
handler(*params)
|
||||
@property
|
||||
def msg_id(self) -> MsgId | None:
|
||||
"""Returns the id of the current context (if it exists)."""
|
||||
ctx = contextvars.copy_context()
|
||||
return ctx.get(self._ctx_msg_id)
|
||||
|
||||
def _execute_notification_callback(self, future):
|
||||
"""Success callback used for coroutine notification message."""
|
||||
if future.exception():
|
||||
try:
|
||||
raise future.exception()
|
||||
except Exception:
|
||||
error = JsonRpcInternalError.of(sys.exc_info())
|
||||
logger.exception('Exception occurred in notification: "%s"', error)
|
||||
def _execute_handler(
|
||||
self,
|
||||
msg_id: MsgId,
|
||||
handler: MessageHandler,
|
||||
callback: MessageCallback,
|
||||
args: tuple[Any, ...] | None = None,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Execute the given message handler.
|
||||
|
||||
# Revisit. Client does not support response with msg_id = None
|
||||
# https://stackoverflow.com/questions/31091376/json-rpc-2-0-allow-notifications-to-have-an-error-response
|
||||
# self._send_response(None, error=error)
|
||||
Parameters
|
||||
----------
|
||||
msg_id
|
||||
The id of the message being handled
|
||||
|
||||
def _execute_request(self, msg_id, handler, params):
|
||||
"""Executes request message handler."""
|
||||
handler
|
||||
The request handler to call
|
||||
|
||||
callback
|
||||
An optional callback function to call upon completion of the handler
|
||||
|
||||
args
|
||||
Positional arguments to pass to the handler
|
||||
|
||||
kwargs
|
||||
Keyword arguments to pass to the handler
|
||||
"""
|
||||
future: Future[Any]
|
||||
args = args or tuple()
|
||||
kwargs = kwargs or {}
|
||||
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
future = asyncio.ensure_future(handler(params))
|
||||
future = asyncio.ensure_future(handler(*args, **kwargs))
|
||||
self._request_futures[msg_id] = future
|
||||
future.add_done_callback(partial(self._execute_request_callback, msg_id))
|
||||
future.add_done_callback(callback)
|
||||
|
||||
elif is_thread_function(handler):
|
||||
future = self._server.thread_pool.submit(handler, *args, **kwargs)
|
||||
self._request_futures[msg_id] = future
|
||||
future.add_done_callback(callback)
|
||||
|
||||
elif inspect.isgeneratorfunction(handler):
|
||||
future = Future()
|
||||
self._request_futures[msg_id] = future
|
||||
future.add_done_callback(callback)
|
||||
|
||||
try:
|
||||
self._run_generator(
|
||||
future=None, gen=handler(*args, **kwargs), result_future=future
|
||||
)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
else:
|
||||
# Can't be canceled
|
||||
if is_thread_function(handler):
|
||||
self._server.thread_pool.apply_async(
|
||||
handler,
|
||||
(params,),
|
||||
callback=partial(
|
||||
self._send_response,
|
||||
msg_id,
|
||||
),
|
||||
error_callback=partial(self._execute_request_err_callback, msg_id),
|
||||
)
|
||||
else:
|
||||
self._send_response(msg_id, handler(params))
|
||||
# While a future is not necessary for a synchronous function, it allows us to use a single
|
||||
# pattern across all handler types
|
||||
future = Future()
|
||||
future.add_done_callback(callback)
|
||||
|
||||
try:
|
||||
result = handler(*args, **kwargs)
|
||||
future.set_result(result)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
def _run_generator(
|
||||
self,
|
||||
future: Future[Any] | None,
|
||||
*,
|
||||
gen: Generator[Any, Any, Any],
|
||||
result_future: Future[Any],
|
||||
):
|
||||
"""Run the next portion of the given generator.
|
||||
|
||||
Generator handlers are designed to ``yield`` to other handlers that are executed
|
||||
separately before their results are sent back into the generator allowing
|
||||
execution to continue.
|
||||
|
||||
Generator handlers are primarily used in the implementation of pygls' builtin
|
||||
feature handlers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
future
|
||||
The future that contains the result of the previously executed handler, if any
|
||||
|
||||
gen
|
||||
The generator to run
|
||||
|
||||
result_future
|
||||
The future to send the final result to once the generator stops.
|
||||
"""
|
||||
|
||||
if result_future.cancelled():
|
||||
return
|
||||
|
||||
try:
|
||||
value = future.result() if future is not None else None
|
||||
handler, args, kwargs = gen.send(value)
|
||||
|
||||
self._execute_handler(
|
||||
str(uuid.uuid4()),
|
||||
handler,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
callback=partial(
|
||||
self._run_generator, gen=gen, result_future=result_future
|
||||
),
|
||||
)
|
||||
except StopIteration as result:
|
||||
result_future.set_result(result.value)
|
||||
|
||||
except Exception as exc:
|
||||
result_future.set_exception(exc)
|
||||
|
||||
def _send_handler_result(self, future: Future[Any], *, msg_id: MsgId):
|
||||
"""Callback function that sends the result of the given future to the client.
|
||||
|
||||
Used to respond to request messages.
|
||||
"""
|
||||
self._request_futures.pop(msg_id, None)
|
||||
|
||||
def _execute_request_callback(self, msg_id, future):
|
||||
"""Success callback used for coroutine request message."""
|
||||
try:
|
||||
if not future.cancelled():
|
||||
self._send_response(msg_id, result=future.result())
|
||||
@@ -199,30 +305,41 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
f'Request with id "{msg_id}" is canceled'
|
||||
).to_response_error(),
|
||||
)
|
||||
self._request_futures.pop(msg_id, None)
|
||||
except JsonRpcException as exc:
|
||||
logger.exception('Exception occurred for message "%s"', msg_id)
|
||||
self._send_response(msg_id, error=exc.to_response_error())
|
||||
self._server._report_server_error(exc, FeatureRequestError)
|
||||
|
||||
except Exception:
|
||||
error = JsonRpcInternalError.of(sys.exc_info())
|
||||
logger.exception('Exception occurred for message "%s": %s', msg_id, error)
|
||||
logger.exception('Exception occurred for message "%s"', msg_id)
|
||||
self._send_response(msg_id, error=error.to_response_error())
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
|
||||
def _execute_request_err_callback(self, msg_id, exc):
|
||||
"""Error callback used for coroutine request message."""
|
||||
exc_info = (type(exc), exc, None)
|
||||
error = JsonRpcInternalError.of(exc_info)
|
||||
logger.exception('Exception occurred for message "%s": %s', msg_id, error)
|
||||
self._send_response(msg_id, error=error.to_response_error())
|
||||
def _check_handler_result(self, future: Future[Any]):
|
||||
"""Check the result of the future to see if an error occurred.
|
||||
|
||||
def _get_handler(self, feature_name):
|
||||
"""Returns builtin or used defined feature by name if exists."""
|
||||
try:
|
||||
return self.fm.builtin_features[feature_name]
|
||||
except KeyError:
|
||||
Used when handling notification messages
|
||||
"""
|
||||
if not future.cancelled() and (exc := future.exception()) is not None:
|
||||
try:
|
||||
return self.fm.features[feature_name]
|
||||
except KeyError:
|
||||
raise JsonRpcMethodNotFound.of(feature_name)
|
||||
raise exc
|
||||
except Exception:
|
||||
error = JsonRpcInternalError.of(sys.exc_info())
|
||||
self._server._report_server_error(error, FeatureNotificationError)
|
||||
|
||||
def _handle_cancel_notification(self, msg_id):
|
||||
def _get_handler(self, feature_name: str) -> MessageHandler:
|
||||
"""Returns builtin or used defined feature by name if exists."""
|
||||
|
||||
if (handler := self.fm.builtin_features.get(feature_name)) is not None:
|
||||
return handler
|
||||
|
||||
if (handler := self.fm.features.get(feature_name)) is not None:
|
||||
return handler
|
||||
|
||||
raise JsonRpcMethodNotFound.of(feature_name)
|
||||
|
||||
def _handle_cancel_notification(self, msg_id: MsgId):
|
||||
"""Handles a cancel notification from the client."""
|
||||
future = self._request_futures.pop(msg_id, None)
|
||||
|
||||
@@ -234,7 +351,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
if future.cancel():
|
||||
logger.info('Cancelled request with id "%s"', msg_id)
|
||||
|
||||
def _handle_notification(self, method_name, params):
|
||||
def _handle_notification(self, method_name: str, params: Any):
|
||||
"""Handles a notification from the client."""
|
||||
if method_name == CANCEL_REQUEST:
|
||||
self._handle_cancel_notification(params.id)
|
||||
@@ -242,29 +359,45 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
try:
|
||||
handler = self._get_handler(method_name)
|
||||
self._execute_notification(handler, params)
|
||||
except (KeyError, JsonRpcMethodNotFound):
|
||||
logger.warning('Ignoring notification for unknown method "%s"', method_name)
|
||||
self._execute_handler(
|
||||
msg_id=str(uuid.uuid4()),
|
||||
handler=handler,
|
||||
args=(params,),
|
||||
callback=self._check_handler_result,
|
||||
)
|
||||
except JsonRpcMethodNotFound:
|
||||
logger.warning("Ignoring notification for unknown method %r", method_name)
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
'Failed to handle notification "%s": %s',
|
||||
"Failed to handle notification %r: %s",
|
||||
method_name,
|
||||
params,
|
||||
exc_info=True,
|
||||
)
|
||||
self._server._report_server_error(error, FeatureNotificationError)
|
||||
|
||||
def _handle_request(self, msg_id, method_name, params):
|
||||
def _handle_request(self, msg_id: MsgId, method_name: str, params: Any):
|
||||
"""Handles a request from the client."""
|
||||
try:
|
||||
handler = self._get_handler(method_name)
|
||||
|
||||
# workspace/executeCommand is a special case
|
||||
if method_name == WORKSPACE_EXECUTE_COMMAND:
|
||||
handler(params, msg_id)
|
||||
else:
|
||||
self._execute_request(msg_id, handler, params)
|
||||
# Set the request id within the current context.
|
||||
self._ctx_msg_id.set(msg_id)
|
||||
self._execute_handler(
|
||||
msg_id=msg_id,
|
||||
handler=handler,
|
||||
args=(params,),
|
||||
callback=partial(self._send_handler_result, msg_id=msg_id),
|
||||
)
|
||||
|
||||
except JsonRpcMethodNotFound as error:
|
||||
logger.warning(
|
||||
"Failed to handle request %r, unknown method %r",
|
||||
msg_id,
|
||||
method_name,
|
||||
)
|
||||
self._send_response(msg_id, None, error.to_response_error())
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
except JsonRpcException as error:
|
||||
logger.exception(
|
||||
"Failed to handle request %s %s %s",
|
||||
@@ -287,7 +420,12 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
self._send_response(msg_id, None, err)
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
|
||||
def _handle_response(self, msg_id, result=None, error=None):
|
||||
def _handle_response(
|
||||
self,
|
||||
msg_id: MsgId,
|
||||
result: Any | None = None,
|
||||
error: ResponseError | None = None,
|
||||
):
|
||||
"""Handles a response from the client."""
|
||||
future = self._request_futures.pop(msg_id, None)
|
||||
|
||||
@@ -302,7 +440,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
logger.debug('Received result for message "%s": %s', msg_id, result)
|
||||
future.set_result(result)
|
||||
|
||||
def _serialize_message(self, data):
|
||||
def _serialize_message(self, data: Any) -> dict[str, Any]:
|
||||
"""Function used to serialize data sent to the client."""
|
||||
|
||||
if hasattr(data, "__attrs_attrs__"):
|
||||
@@ -313,7 +451,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
return data.__dict__
|
||||
|
||||
def _deserialize_message(self, data):
|
||||
def structure_message(self, data: dict[str, Any]):
|
||||
"""Function used to deserialize data recevied from the client."""
|
||||
|
||||
if "jsonrpc" not in data:
|
||||
@@ -330,7 +468,8 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
return self._converter.structure(data, request_type)
|
||||
else:
|
||||
response_type = (
|
||||
self._result_types.pop(data["id"]) or JsonRPCResponseMessage
|
||||
self._result_types.pop(data["id"], None)
|
||||
or JsonRPCResponseMessage
|
||||
)
|
||||
return self._converter.structure(data, response_type)
|
||||
|
||||
@@ -347,7 +486,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
logger.error("Unable to deserialize message\n%s", traceback.format_exc())
|
||||
raise JsonRpcInternalError() from exc
|
||||
|
||||
def _procedure_handler(self, message):
|
||||
def handle_message(self, message: RPCMessage):
|
||||
"""Delegates message to handlers depending on message type."""
|
||||
|
||||
if message.jsonrpc != JsonRPCProtocol.VERSION:
|
||||
@@ -358,27 +497,31 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
logger.warning("Server shutting down. No more requests!")
|
||||
return
|
||||
|
||||
if hasattr(message, "method"):
|
||||
if hasattr(message, "id"):
|
||||
logger.debug("Request message received.")
|
||||
self._handle_request(message.id, message.method, message.params)
|
||||
else:
|
||||
logger.debug("Notification message received.")
|
||||
self._handle_notification(message.method, message.params)
|
||||
else:
|
||||
if hasattr(message, "error"):
|
||||
logger.debug("Error message received.")
|
||||
self._handle_response(message.id, None, message.error)
|
||||
else:
|
||||
logger.debug("Response message received.")
|
||||
self._handle_response(message.id, message.result)
|
||||
# Run each handler within its own context.
|
||||
ctx = contextvars.copy_context()
|
||||
|
||||
def _send_data(self, data):
|
||||
if isinstance(message, RPCRequest):
|
||||
logger.debug("Request %r received", message.method)
|
||||
ctx.run(self._handle_request, message.id, message.method, message.params)
|
||||
|
||||
elif isinstance(message, RPCNotification):
|
||||
logger.debug("Notification %r received", message.method)
|
||||
ctx.run(self._handle_notification, message.method, message.params)
|
||||
|
||||
elif isinstance(message, RPCResponse):
|
||||
logger.debug("Response message received.")
|
||||
ctx.run(self._handle_response, message.id, message.result)
|
||||
|
||||
else:
|
||||
logger.debug("Error message received.")
|
||||
ctx.run(self._handle_response, message.id, None, message.error)
|
||||
|
||||
def _send_data(self, data: Any):
|
||||
"""Sends data to the client."""
|
||||
if not data:
|
||||
return
|
||||
|
||||
if self.transport is None:
|
||||
if self.writer is None:
|
||||
logger.error("Unable to send data, no available transport!")
|
||||
return
|
||||
|
||||
@@ -386,31 +529,49 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
body = json.dumps(data, default=self._serialize_message)
|
||||
logger.info("Sending data: %s", body)
|
||||
|
||||
if self._send_only_body:
|
||||
# Mypy/Pyright seem to think `write()` wants `"bytes | bytearray | memoryview"`
|
||||
# But runtime errors with anything but `str`.
|
||||
self.transport.write(body) # type: ignore
|
||||
return
|
||||
if self._include_headers:
|
||||
header = (
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
|
||||
)
|
||||
data = header + body
|
||||
else:
|
||||
data = body
|
||||
|
||||
header = (
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
|
||||
).encode(self.CHARSET)
|
||||
res = self.writer.write(data.encode(self.CHARSET))
|
||||
if inspect.isawaitable(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
self.transport.write(header + body.encode(self.CHARSET))
|
||||
except BrokenPipeError:
|
||||
logger.exception("Error sending data. BrokenPipeError", exc_info=True)
|
||||
raise
|
||||
except Exception as error:
|
||||
logger.exception("Error sending data", exc_info=True)
|
||||
self._server._report_server_error(error, JsonRpcInternalError)
|
||||
|
||||
def _send_response(
|
||||
self, msg_id, result=None, error: Union[ResponseError, None] = None
|
||||
self,
|
||||
msg_id: MsgId,
|
||||
result: Any | None = None,
|
||||
error: Union[ResponseError, None] = None,
|
||||
):
|
||||
"""Sends a JSON RPC response to the client.
|
||||
"""Send a JSON-RPC response
|
||||
|
||||
Args:
|
||||
msg_id(str): Id from request
|
||||
result(any): Result returned by handler
|
||||
error(any): Error returned by handler
|
||||
.. important::
|
||||
|
||||
You should only set ``result`` OR ``error``.
|
||||
If both are set, then the ``result`` value will be ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
msg_id
|
||||
The id of the message to respond to
|
||||
|
||||
result
|
||||
The result to send in the event of a success
|
||||
|
||||
error
|
||||
The error to send in the event of a failure
|
||||
"""
|
||||
|
||||
if error is not None:
|
||||
@@ -424,70 +585,51 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
self._send_data(response)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Method from base class, called when connection is lost, in which case we
|
||||
want to shutdown the server's process as well.
|
||||
"""
|
||||
logger.error("Connection to the client is lost! Shutting down the server.")
|
||||
sys.exit(1)
|
||||
|
||||
def connection_made( # type: ignore # see: https://github.com/python/typeshed/issues/3021
|
||||
def set_writer(
|
||||
self,
|
||||
transport: asyncio.Transport,
|
||||
writer: AsyncWriter | Writer,
|
||||
include_headers: bool = True,
|
||||
):
|
||||
"""Method from base class, called when connection is established"""
|
||||
self.transport = transport
|
||||
"""Set the writer object to use when sending data
|
||||
|
||||
def data_received(self, data: bytes):
|
||||
try:
|
||||
self._data_received(data)
|
||||
except Exception as error:
|
||||
logger.exception("Error receiving data", exc_info=True)
|
||||
self._server._report_server_error(error, JsonRpcInternalError)
|
||||
Parameters
|
||||
----------
|
||||
writer
|
||||
The writer object
|
||||
|
||||
def _data_received(self, data: bytes):
|
||||
"""Method from base class, called when server receives the data"""
|
||||
logger.debug("Received %r", data)
|
||||
include_headers
|
||||
Flag indicating if headers like ``Content-Length`` should be included when
|
||||
sending data. (Default ``True``)
|
||||
"""
|
||||
self.writer = writer
|
||||
self._include_headers = include_headers
|
||||
|
||||
while len(data):
|
||||
# Append the incoming chunk to the message buffer
|
||||
self._message_buf.append(data)
|
||||
|
||||
# Look for the body of the message
|
||||
message = b"".join(self._message_buf)
|
||||
found = JsonRPCProtocol.MESSAGE_PATTERN.fullmatch(message)
|
||||
|
||||
body = found.group("body") if found else b""
|
||||
length = int(found.group("length")) if found else 1
|
||||
|
||||
if len(body) < length:
|
||||
# Message is incomplete; bail until more data arrives
|
||||
return
|
||||
|
||||
# Message is complete;
|
||||
# extract the body and any remaining data,
|
||||
# and reset the buffer for the next message
|
||||
body, data = body[:length], body[length:]
|
||||
self._message_buf = []
|
||||
|
||||
# Parse the body
|
||||
self._procedure_handler(
|
||||
json.loads(
|
||||
body.decode(self.CHARSET), object_hook=self._deserialize_message
|
||||
)
|
||||
)
|
||||
|
||||
def get_message_type(self, method: str) -> Optional[Type]:
|
||||
def get_message_type(self, method: str) -> Type[Any] | None:
|
||||
"""Return the type definition of the message associated with the given method."""
|
||||
return None
|
||||
|
||||
def get_result_type(self, method: str) -> Optional[Type]:
|
||||
def get_result_type(self, method: str) -> Type[Any] | None:
|
||||
"""Return the type definition of the result associated with the given method."""
|
||||
return None
|
||||
|
||||
def notify(self, method: str, params=None):
|
||||
"""Sends a JSON RPC notification to the client."""
|
||||
def notify(self, method: str, params: Any | None = None):
|
||||
"""Send a JSON-RPC notification.
|
||||
|
||||
.. note::
|
||||
|
||||
Notifications are "fire-and-forget", there is no way for the recipient to
|
||||
respond directly to a notification. If you expect a response to this message,
|
||||
use ``send_request``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method
|
||||
The method name of the message to send
|
||||
|
||||
params
|
||||
The payload of the message
|
||||
|
||||
"""
|
||||
logger.debug("Sending notification: '%s' %s", method, params)
|
||||
|
||||
notification_type = self.get_message_type(method) or JsonRPCNotification
|
||||
@@ -497,15 +639,35 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
self._send_data(notification)
|
||||
|
||||
def send_request(self, method, params=None, callback=None, msg_id=None):
|
||||
"""Sends a JSON RPC request to the client.
|
||||
def send_request(
|
||||
self,
|
||||
method: str,
|
||||
params: Any | None = None,
|
||||
callback: Callable[[Any], None] | None = None,
|
||||
msg_id: MsgId | None = None,
|
||||
) -> Future[Any]:
|
||||
"""Send a JSON-RPC request
|
||||
|
||||
Args:
|
||||
method(str): The method name of the message to send
|
||||
params(any): The payload of the message
|
||||
Parameters
|
||||
----------
|
||||
method
|
||||
The method name of the message to send
|
||||
|
||||
Returns:
|
||||
Future that will be resolved once a response has been received
|
||||
params
|
||||
The payload of the message
|
||||
|
||||
callback
|
||||
If set, the given callback will be called with the result of the future
|
||||
when it resolves
|
||||
|
||||
msg_id
|
||||
Send the request using the given id, if ``None``, an id will be automatically
|
||||
generated
|
||||
|
||||
Returns
|
||||
-------
|
||||
Future[Any]
|
||||
A future that will resolve once a response has been received
|
||||
"""
|
||||
|
||||
if msg_id is None:
|
||||
@@ -521,12 +683,12 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
jsonrpc=JsonRPCProtocol.VERSION,
|
||||
)
|
||||
|
||||
future = Future() # type: ignore[var-annotated]
|
||||
future: Future[Any] = Future()
|
||||
# If callback function is given, call it when result is received
|
||||
if callback:
|
||||
|
||||
def wrapper(future: Future):
|
||||
result = future.result()
|
||||
def wrapper(fut: Future[Any]):
|
||||
result = fut.result()
|
||||
logger.info("Client response for %s received: %s", params, result)
|
||||
callback(result)
|
||||
|
||||
@@ -539,22 +701,35 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
return future
|
||||
|
||||
def send_request_async(self, method, params=None, msg_id=None):
|
||||
"""Calls `send_request` and wraps `concurrent.futures.Future` with
|
||||
`asyncio.Future` so it can be used with `await` keyword.
|
||||
def send_request_async(
|
||||
self, method: str, params: Any | None = None, msg_id: MsgId | None = None
|
||||
):
|
||||
"""Send a JSON-RPC request, asynchronously.
|
||||
|
||||
Args:
|
||||
method(str): The method name of the message to send
|
||||
params(any): The payload of the message
|
||||
msg_id(str|int): Optional, message id
|
||||
This method calls `send_request`, wrapping the resulting future with
|
||||
``asyncio.wrap_future`` so it can be used in an ``async def`` function and
|
||||
awaited with the ``await`` keyword.
|
||||
|
||||
Returns:
|
||||
`asyncio.Future` that can be awaited
|
||||
Parameters
|
||||
----------
|
||||
method
|
||||
The method name of the message to send
|
||||
|
||||
params
|
||||
The payload of the message
|
||||
|
||||
callback
|
||||
If set, the given callback will be called with the result of the future
|
||||
when it resolves
|
||||
|
||||
msg_id
|
||||
Send the request using the given id, if ``None``, an id will be automatically
|
||||
generated
|
||||
|
||||
Returns
|
||||
-------
|
||||
`asyncio.Future` that can be awaited
|
||||
"""
|
||||
return asyncio.wrap_future(
|
||||
self.send_request(method, params=params, msg_id=msg_id)
|
||||
)
|
||||
|
||||
def thread(self):
|
||||
"""Decorator that mark function to execute it in a thread."""
|
||||
return self.fm.thread()
|
||||
|
||||
@@ -15,88 +15,34 @@
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from concurrent.futures import Future
|
||||
import typing
|
||||
from functools import lru_cache
|
||||
from itertools import zip_longest
|
||||
from typing import (
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.capabilities import ServerCapabilitiesBuilder
|
||||
from pygls.lsp import ConfigCallbackType, ShowDocumentCallbackType
|
||||
from lsprotocol.types import (
|
||||
CLIENT_REGISTER_CAPABILITY,
|
||||
CLIENT_UNREGISTER_CAPABILITY,
|
||||
EXIT,
|
||||
INITIALIZE,
|
||||
INITIALIZED,
|
||||
METHOD_TO_TYPES,
|
||||
NOTEBOOK_DOCUMENT_DID_CHANGE,
|
||||
NOTEBOOK_DOCUMENT_DID_CLOSE,
|
||||
NOTEBOOK_DOCUMENT_DID_OPEN,
|
||||
LOG_TRACE,
|
||||
SET_TRACE,
|
||||
SHUTDOWN,
|
||||
TEXT_DOCUMENT_DID_CHANGE,
|
||||
TEXT_DOCUMENT_DID_CLOSE,
|
||||
TEXT_DOCUMENT_DID_OPEN,
|
||||
TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
|
||||
WINDOW_LOG_MESSAGE,
|
||||
WINDOW_SHOW_DOCUMENT,
|
||||
WINDOW_SHOW_MESSAGE,
|
||||
WINDOW_WORK_DONE_PROGRESS_CANCEL,
|
||||
WORKSPACE_APPLY_EDIT,
|
||||
WORKSPACE_CONFIGURATION,
|
||||
WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS,
|
||||
WORKSPACE_EXECUTE_COMMAND,
|
||||
WORKSPACE_SEMANTIC_TOKENS_REFRESH,
|
||||
)
|
||||
from lsprotocol.types import (
|
||||
ApplyWorkspaceEditParams,
|
||||
Diagnostic,
|
||||
DidChangeNotebookDocumentParams,
|
||||
DidChangeTextDocumentParams,
|
||||
DidChangeWorkspaceFoldersParams,
|
||||
DidCloseNotebookDocumentParams,
|
||||
DidCloseTextDocumentParams,
|
||||
DidOpenNotebookDocumentParams,
|
||||
DidOpenTextDocumentParams,
|
||||
ExecuteCommandParams,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
LogMessageParams,
|
||||
LogTraceParams,
|
||||
MessageType,
|
||||
PublishDiagnosticsParams,
|
||||
RegistrationParams,
|
||||
SetTraceParams,
|
||||
ShowDocumentParams,
|
||||
ShowMessageParams,
|
||||
TraceValues,
|
||||
UnregistrationParams,
|
||||
WorkspaceApplyEditResponse,
|
||||
WorkspaceEdit,
|
||||
InitializeResultServerInfoType,
|
||||
WorkspaceConfigurationParams,
|
||||
WorkDoneProgressCancelParams,
|
||||
)
|
||||
from pygls.constants import PARAM_LS
|
||||
from pygls.exceptions import JsonRpcInvalidParams
|
||||
from pygls.protocol.json_rpc import JsonRPCProtocol
|
||||
from pygls.protocol.lsp_meta import LSPMeta
|
||||
from pygls.uris import from_fs_path
|
||||
from pygls.workspace import Workspace
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from typing import Any, Callable, Optional, Type, TypeVar
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
from cattrs import Converter
|
||||
|
||||
from pygls.lsp.server import LanguageServer
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -109,7 +55,7 @@ def lsp_method(method_name: str) -> Callable[[F], F]:
|
||||
return decorator
|
||||
|
||||
|
||||
class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
class LanguageServerProtocol(JsonRPCProtocol):
|
||||
"""A class that represents language server protocol.
|
||||
|
||||
It contains implementations for generic LSP features.
|
||||
@@ -118,17 +64,19 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
workspace(Workspace): In memory workspace
|
||||
"""
|
||||
|
||||
def __init__(self, server, converter):
|
||||
_server: LanguageServer
|
||||
|
||||
def __init__(self, server: LanguageServer, converter: Converter):
|
||||
super().__init__(server, converter)
|
||||
|
||||
self._workspace: Optional[Workspace] = None
|
||||
self.trace = None
|
||||
self.trace = types.TraceValue.Off
|
||||
|
||||
from pygls.progress import Progress
|
||||
|
||||
self.progress = Progress(self)
|
||||
|
||||
self.server_info = InitializeResultServerInfoType(
|
||||
self.server_info = types.ServerInfo(
|
||||
name=server.name,
|
||||
version=server.version,
|
||||
)
|
||||
@@ -155,40 +103,38 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
return self._workspace
|
||||
|
||||
@lru_cache()
|
||||
def get_message_type(self, method: str) -> Optional[Type]:
|
||||
def get_message_type(self, method: str) -> Type[Any] | None:
|
||||
"""Return LSP type definitions, as provided by `lsprotocol`"""
|
||||
return METHOD_TO_TYPES.get(method, (None,))[0]
|
||||
return types.METHOD_TO_TYPES.get(method, (None,))[0]
|
||||
|
||||
@lru_cache()
|
||||
def get_result_type(self, method: str) -> Optional[Type]:
|
||||
return METHOD_TO_TYPES.get(method, (None, None))[1]
|
||||
def get_result_type(self, method: str) -> Type[Any] | None:
|
||||
return types.METHOD_TO_TYPES.get(method, (None, None))[1]
|
||||
|
||||
def apply_edit(
|
||||
self, edit: WorkspaceEdit, label: Optional[str] = None
|
||||
) -> WorkspaceApplyEditResponse:
|
||||
"""Sends apply edit request to the client."""
|
||||
return self.send_request(
|
||||
WORKSPACE_APPLY_EDIT, ApplyWorkspaceEditParams(edit=edit, label=label)
|
||||
)
|
||||
|
||||
def apply_edit_async(
|
||||
self, edit: WorkspaceEdit, label: Optional[str] = None
|
||||
) -> WorkspaceApplyEditResponse:
|
||||
"""Sends apply edit request to the client. Should be called with `await`"""
|
||||
return self.send_request_async(
|
||||
WORKSPACE_APPLY_EDIT, ApplyWorkspaceEditParams(edit=edit, label=label)
|
||||
)
|
||||
|
||||
@lsp_method(EXIT)
|
||||
def lsp_exit(self, *args) -> None:
|
||||
@lsp_method(types.EXIT)
|
||||
def lsp_exit(self, *args) -> Generator[Any, Any, None]:
|
||||
"""Stops the server process."""
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
|
||||
sys.exit(0 if self._shutdown else 1)
|
||||
# Ensure that the user handler is called first
|
||||
if (user_handler := self.fm.features.get(types.EXIT)) is not None:
|
||||
yield user_handler, args, None
|
||||
|
||||
@lsp_method(INITIALIZE)
|
||||
def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
|
||||
returncode = 0 if self._shutdown else 1
|
||||
if self.writer is None:
|
||||
sys.exit(returncode)
|
||||
|
||||
res = self.writer.close()
|
||||
if inspect.isawaitable(res):
|
||||
# Only call sys.exit once the close task has completed.
|
||||
fut = asyncio.ensure_future(res)
|
||||
fut.add_done_callback(lambda t: sys.exit(returncode))
|
||||
else:
|
||||
sys.exit(returncode)
|
||||
|
||||
@lsp_method(types.INITIALIZE)
|
||||
def lsp_initialize(
|
||||
self, params: types.InitializeParams
|
||||
) -> Generator[Any, Any, types.InitializeResult]:
|
||||
"""Method that initializes language server.
|
||||
It will compute and return server capabilities based on
|
||||
registered features.
|
||||
@@ -200,19 +146,9 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
text_document_sync_kind = self._server._text_document_sync_kind
|
||||
notebook_document_sync = self._server._notebook_document_sync
|
||||
|
||||
# Initialize server capabilities
|
||||
self.client_capabilities = params.capabilities
|
||||
self.server_capabilities = ServerCapabilitiesBuilder(
|
||||
self.client_capabilities,
|
||||
set({**self.fm.features, **self.fm.builtin_features}.keys()),
|
||||
self.fm.feature_options,
|
||||
list(self.fm.commands.keys()),
|
||||
text_document_sync_kind,
|
||||
notebook_document_sync,
|
||||
).build()
|
||||
logger.debug(
|
||||
"Server capabilities: %s",
|
||||
json.dumps(self.server_capabilities, default=self._serialize_message),
|
||||
position_encoding = ServerCapabilitiesBuilder.choose_position_encoding(
|
||||
self.client_capabilities
|
||||
)
|
||||
|
||||
root_path = params.root_path
|
||||
@@ -220,86 +156,144 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
if root_path is not None and root_uri is None:
|
||||
root_uri = from_fs_path(root_path)
|
||||
|
||||
# Initialize the workspace
|
||||
# Initialize the workspace before yielding to the user's initialize handler
|
||||
workspace_folders = params.workspace_folders or []
|
||||
self._workspace = Workspace(
|
||||
root_uri,
|
||||
text_document_sync_kind,
|
||||
workspace_folders,
|
||||
self.server_capabilities.position_encoding,
|
||||
position_encoding,
|
||||
)
|
||||
|
||||
self.trace = TraceValues.Off
|
||||
if (user_handler := self.fm.features.get(types.INITIALIZE)) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
return InitializeResult(
|
||||
# Now that the user has had the opportunity to setup additional features, calculate
|
||||
# the server's capabilities
|
||||
self.server_capabilities = ServerCapabilitiesBuilder(
|
||||
self.client_capabilities,
|
||||
set({**self.fm.features, **self.fm.builtin_features}.keys()),
|
||||
self.fm.feature_options,
|
||||
list(self.fm.commands.keys()),
|
||||
text_document_sync_kind,
|
||||
notebook_document_sync,
|
||||
position_encoding,
|
||||
).build()
|
||||
logger.debug(
|
||||
"Server capabilities: %s",
|
||||
json.dumps(self.server_capabilities, default=self._serialize_message),
|
||||
)
|
||||
|
||||
return types.InitializeResult(
|
||||
capabilities=self.server_capabilities,
|
||||
server_info=self.server_info,
|
||||
)
|
||||
|
||||
@lsp_method(INITIALIZED)
|
||||
def lsp_initialized(self, *args) -> None:
|
||||
@lsp_method(types.INITIALIZED)
|
||||
def lsp_initialized(self, *args):
|
||||
"""Notification received when client and server are connected."""
|
||||
pass
|
||||
|
||||
@lsp_method(SHUTDOWN)
|
||||
def lsp_shutdown(self, *args) -> None:
|
||||
if (user_handler := self.fm.features.get(types.INITIALIZED)) is not None:
|
||||
yield user_handler, args, None
|
||||
|
||||
@lsp_method(types.SHUTDOWN)
|
||||
def lsp_shutdown(self, *args) -> Generator[Any, Any, None]:
|
||||
"""Request from client which asks server to shutdown."""
|
||||
for future in self._request_futures.values():
|
||||
future.cancel()
|
||||
|
||||
if (user_handler := self.fm.features.get(types.SHUTDOWN)) is not None:
|
||||
yield user_handler, args, None
|
||||
|
||||
# Don't cancel the future for this request!
|
||||
current_id = self.msg_id
|
||||
|
||||
for msg_id, future in self._request_futures.items():
|
||||
if msg_id != current_id and not future.done():
|
||||
future.cancel()
|
||||
|
||||
self._shutdown = True
|
||||
return None
|
||||
|
||||
@lsp_method(TEXT_DOCUMENT_DID_CHANGE)
|
||||
def lsp_text_document__did_change(
|
||||
self, params: DidChangeTextDocumentParams
|
||||
) -> None:
|
||||
@lsp_method(types.TEXT_DOCUMENT_DID_CHANGE)
|
||||
def lsp_text_document__did_change(self, params: types.DidChangeTextDocumentParams):
|
||||
"""Updates document's content.
|
||||
(Incremental(from server capabilities); not configurable for now)
|
||||
"""
|
||||
for change in params.content_changes:
|
||||
self.workspace.update_text_document(params.text_document, change)
|
||||
|
||||
@lsp_method(TEXT_DOCUMENT_DID_CLOSE)
|
||||
def lsp_text_document__did_close(self, params: DidCloseTextDocumentParams) -> None:
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.TEXT_DOCUMENT_DID_CHANGE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.TEXT_DOCUMENT_DID_CLOSE)
|
||||
def lsp_text_document__did_close(self, params: types.DidCloseTextDocumentParams):
|
||||
"""Removes document from workspace."""
|
||||
self.workspace.remove_text_document(params.text_document.uri)
|
||||
|
||||
@lsp_method(TEXT_DOCUMENT_DID_OPEN)
|
||||
def lsp_text_document__did_open(self, params: DidOpenTextDocumentParams) -> None:
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.TEXT_DOCUMENT_DID_CLOSE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.TEXT_DOCUMENT_DID_OPEN)
|
||||
def lsp_text_document__did_open(self, params: types.DidOpenTextDocumentParams):
|
||||
"""Puts document to the workspace."""
|
||||
self.workspace.put_text_document(params.text_document)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_OPEN)
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.TEXT_DOCUMENT_DID_OPEN)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.NOTEBOOK_DOCUMENT_DID_OPEN)
|
||||
def lsp_notebook_document__did_open(
|
||||
self, params: DidOpenNotebookDocumentParams
|
||||
) -> None:
|
||||
self, params: types.DidOpenNotebookDocumentParams
|
||||
):
|
||||
"""Put a notebook document into the workspace"""
|
||||
self.workspace.put_notebook_document(params)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_CHANGE)
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.NOTEBOOK_DOCUMENT_DID_OPEN)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.NOTEBOOK_DOCUMENT_DID_CHANGE)
|
||||
def lsp_notebook_document__did_change(
|
||||
self, params: DidChangeNotebookDocumentParams
|
||||
) -> None:
|
||||
self, params: types.DidChangeNotebookDocumentParams
|
||||
):
|
||||
"""Update a notebook's contents"""
|
||||
self.workspace.update_notebook_document(params)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_CLOSE)
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.NOTEBOOK_DOCUMENT_DID_CHANGE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.NOTEBOOK_DOCUMENT_DID_CLOSE)
|
||||
def lsp_notebook_document__did_close(
|
||||
self, params: DidCloseNotebookDocumentParams
|
||||
) -> None:
|
||||
self, params: types.DidCloseNotebookDocumentParams
|
||||
):
|
||||
"""Remove a notebook document from the workspace."""
|
||||
self.workspace.remove_notebook_document(params)
|
||||
|
||||
@lsp_method(SET_TRACE)
|
||||
def lsp_set_trace(self, params: SetTraceParams) -> None:
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.NOTEBOOK_DOCUMENT_DID_CLOSE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.SET_TRACE)
|
||||
def lsp_set_trace(self, params: types.SetTraceParams) -> Generator[Any, Any, None]:
|
||||
"""Changes server trace value."""
|
||||
self.trace = params.value
|
||||
|
||||
@lsp_method(WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS)
|
||||
if (user_handler := self.fm.features.get(types.SET_TRACE)) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS)
|
||||
def lsp_workspace__did_change_workspace_folders(
|
||||
self, params: DidChangeWorkspaceFoldersParams
|
||||
) -> None:
|
||||
self, params: types.DidChangeWorkspaceFoldersParams
|
||||
):
|
||||
"""Adds/Removes folders from the workspace."""
|
||||
logger.info("Workspace folders changed: %s", params)
|
||||
|
||||
@@ -312,18 +306,35 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
if f_remove:
|
||||
self.workspace.remove_folder(f_remove.uri)
|
||||
|
||||
@lsp_method(WORKSPACE_EXECUTE_COMMAND)
|
||||
def lsp_workspace__execute_command(
|
||||
self, params: ExecuteCommandParams, msg_id: str
|
||||
) -> None:
|
||||
"""Executes commands with passed arguments and returns a value."""
|
||||
cmd_handler = self.fm.commands[params.command]
|
||||
self._execute_request(msg_id, cmd_handler, params.arguments)
|
||||
if (
|
||||
user_handler := self.fm.features.get(
|
||||
types.WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS
|
||||
)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(WINDOW_WORK_DONE_PROGRESS_CANCEL)
|
||||
def lsp_work_done_progress_cancel(
|
||||
self, params: WorkDoneProgressCancelParams
|
||||
) -> None:
|
||||
@lsp_method(types.WORKSPACE_EXECUTE_COMMAND)
|
||||
def lsp_workspace__execute_command(
|
||||
self, params: types.ExecuteCommandParams
|
||||
) -> Generator[Any, Any, Any]:
|
||||
"""Executes commands with passed arguments and returns a value."""
|
||||
|
||||
if (handler := self.fm.commands.get(params.command, None)) is None:
|
||||
raise JsonRpcInvalidParams.of(
|
||||
ValueError(f"Command name {params.command!r} is not defined")
|
||||
)
|
||||
|
||||
try:
|
||||
args, kwargs = _prepare_command_arguments(handler, params, self._converter)
|
||||
except Exception as exc:
|
||||
raise JsonRpcInvalidParams.of(exc)
|
||||
|
||||
# Call the user's command handler.
|
||||
result = yield handler, args, kwargs
|
||||
return result
|
||||
|
||||
@lsp_method(types.WINDOW_WORK_DONE_PROGRESS_CANCEL)
|
||||
def lsp_work_done_progress_cancel(self, params: types.WorkDoneProgressCancelParams):
|
||||
"""Received a progress cancellation from client."""
|
||||
future = self.progress.tokens.get(params.token)
|
||||
if future is None:
|
||||
@@ -333,237 +344,85 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
else:
|
||||
future.cancel()
|
||||
|
||||
def get_configuration(
|
||||
self,
|
||||
params: WorkspaceConfigurationParams,
|
||||
callback: Optional[ConfigCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Sends configuration request to the client.
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.WINDOW_WORK_DONE_PROGRESS_CANCEL)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
Args:
|
||||
params(WorkspaceConfigurationParams): WorkspaceConfigurationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(WORKSPACE_CONFIGURATION, params, callback)
|
||||
|
||||
def get_configuration_async(
|
||||
self, params: WorkspaceConfigurationParams
|
||||
) -> asyncio.Future:
|
||||
"""Calls `get_configuration` method but designed to use with coroutines
|
||||
def _prepare_command_arguments(
|
||||
handler: Callable[..., Any],
|
||||
params: types.ExecuteCommandParams,
|
||||
converter: Converter,
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
"""Prepare the arguments to pass to the command handler."""
|
||||
|
||||
Args:
|
||||
params(WorkspaceConfigurationParams): WorkspaceConfigurationParams from lsp specs
|
||||
Returns:
|
||||
asyncio.Future that can be awaited
|
||||
"""
|
||||
return asyncio.wrap_future(self.get_configuration(params))
|
||||
if params.arguments is None:
|
||||
return tuple(), {}
|
||||
|
||||
def log_trace(self, message: str, verbose: Optional[str] = None) -> None:
|
||||
"""Sends trace notification to the client."""
|
||||
if self.trace == TraceValues.Off:
|
||||
return
|
||||
# Import this here to not introduce an import cycle at the module level
|
||||
from pygls.lsp.server import LanguageServer
|
||||
|
||||
params = LogTraceParams(message=message)
|
||||
if verbose and self.trace == TraceValues.Verbose:
|
||||
params.verbose = verbose
|
||||
param_vals = iter(params.arguments)
|
||||
param_defs, annotations = _get_handler_params_annotations(handler)
|
||||
|
||||
self.notify(LOG_TRACE, params)
|
||||
args: list[Any] = []
|
||||
kwargs: dict[str, Any] = {}
|
||||
|
||||
def _publish_diagnostics_deprecator(
|
||||
self,
|
||||
params_or_uri: Union[str, PublishDiagnosticsParams],
|
||||
diagnostics: Optional[List[Diagnostic]],
|
||||
version: Optional[int],
|
||||
**kwargs,
|
||||
) -> PublishDiagnosticsParams:
|
||||
if isinstance(params_or_uri, str):
|
||||
message = "DEPRECATION: "
|
||||
"`publish_diagnostics("
|
||||
"self, doc_uri: str, diagnostics: List[Diagnostic], version: Optional[int] = None)`"
|
||||
"will be replaced with `publish_diagnostics(self, params: PublishDiagnosticsParams)`"
|
||||
logging.warning(message)
|
||||
# param_defs is an OrderedDict so *in theory* at least we don't have to
|
||||
# worry about argument order.
|
||||
found_ls = False
|
||||
for idx, (name, param) in enumerate(param_defs.items()):
|
||||
ptype = annotations.get(name, None)
|
||||
|
||||
# We don't need to provide the injected server instance here.
|
||||
# The @server.command decorator will have already handled it.
|
||||
if idx == 0:
|
||||
if name == PARAM_LS:
|
||||
found_ls = True
|
||||
continue
|
||||
|
||||
if (ptype is not None) and issubclass(ptype, LanguageServer):
|
||||
found_ls = True
|
||||
continue
|
||||
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL: # i.e. *args
|
||||
# consume the remaining values
|
||||
args.extend(param_vals)
|
||||
|
||||
params = self._construct_publish_diagnostic_type(
|
||||
params_or_uri, diagnostics, version, **kwargs
|
||||
)
|
||||
else:
|
||||
params = params_or_uri
|
||||
return params
|
||||
try:
|
||||
value = converter.structure(next(param_vals), ptype)
|
||||
except StopIteration as exc:
|
||||
raise TypeError(
|
||||
f"Expected {len(param_defs) - found_ls} arguments, "
|
||||
f"got {len(params.arguments)}"
|
||||
) from exc
|
||||
|
||||
def _construct_publish_diagnostic_type(
|
||||
self,
|
||||
uri: str,
|
||||
diagnostics: Optional[List[Diagnostic]],
|
||||
version: Optional[int],
|
||||
**kwargs,
|
||||
) -> PublishDiagnosticsParams:
|
||||
if diagnostics is None:
|
||||
diagnostics = []
|
||||
args.append(value)
|
||||
|
||||
args = {
|
||||
**{"uri": uri, "diagnostics": diagnostics, "version": version},
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
params = PublishDiagnosticsParams(**args) # type:ignore
|
||||
return params
|
||||
|
||||
def publish_diagnostics(
|
||||
self,
|
||||
params_or_uri: Union[str, PublishDiagnosticsParams],
|
||||
diagnostics: Optional[List[Diagnostic]] = None,
|
||||
version: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Sends diagnostic notification to the client.
|
||||
|
||||
.. deprecated:: 1.0.1
|
||||
|
||||
Passing ``(uri, diagnostics, version)`` as arguments is deprecated.
|
||||
Pass an instance of :class:`~lsprotocol.types.PublishDiagnosticParams`
|
||||
instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params_or_uri
|
||||
The :class:`~lsprotocol.types.PublishDiagnosticParams` to send to the client.
|
||||
|
||||
diagnostics
|
||||
*Deprecated*. The diagnostics to publish
|
||||
|
||||
version
|
||||
*Deprecated*: The version number
|
||||
"""
|
||||
params = self._publish_diagnostics_deprecator(
|
||||
params_or_uri, diagnostics, version, **kwargs
|
||||
)
|
||||
self.notify(TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS, params)
|
||||
|
||||
def register_capability(
|
||||
self, params: RegistrationParams, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Register a new capability on the client.
|
||||
|
||||
Args:
|
||||
params(RegistrationParams): RegistrationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(CLIENT_REGISTER_CAPABILITY, params, callback)
|
||||
|
||||
def register_capability_async(self, params: RegistrationParams) -> asyncio.Future:
|
||||
"""Register a new capability on the client.
|
||||
|
||||
Args:
|
||||
params(RegistrationParams): RegistrationParams from lsp specs
|
||||
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.register_capability(params, None))
|
||||
|
||||
def semantic_tokens_refresh(
|
||||
self, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Requesting a refresh of all semantic tokens.
|
||||
|
||||
Args:
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(WORKSPACE_SEMANTIC_TOKENS_REFRESH, callback=callback)
|
||||
|
||||
def semantic_tokens_refresh_async(self) -> asyncio.Future:
|
||||
"""Requesting a refresh of all semantic tokens.
|
||||
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.semantic_tokens_refresh(None))
|
||||
|
||||
def show_document(
|
||||
self,
|
||||
params: ShowDocumentParams,
|
||||
callback: Optional[ShowDocumentCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Display a particular document in the user interface.
|
||||
|
||||
Args:
|
||||
params(ShowDocumentParams): ShowDocumentParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(WINDOW_SHOW_DOCUMENT, params, callback)
|
||||
|
||||
def show_document_async(self, params: ShowDocumentParams) -> asyncio.Future:
|
||||
"""Display a particular document in the user interface.
|
||||
|
||||
Args:
|
||||
params(ShowDocumentParams): ShowDocumentParams from lsp specs
|
||||
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.show_document(params, None))
|
||||
|
||||
def show_message(self, message, msg_type=MessageType.Info):
|
||||
"""Sends message to the client to display message."""
|
||||
self.notify(
|
||||
WINDOW_SHOW_MESSAGE, ShowMessageParams(type=msg_type, message=message)
|
||||
# did we consume all the values?
|
||||
if len(list(param_vals)) > 0:
|
||||
raise TypeError(
|
||||
f"Expected {len(param_defs) - found_ls} arguments, "
|
||||
f"got {len(params.arguments)}"
|
||||
)
|
||||
|
||||
def show_message_log(self, message, msg_type=MessageType.Log):
|
||||
"""Sends message to the client's output channel."""
|
||||
self.notify(
|
||||
WINDOW_LOG_MESSAGE, LogMessageParams(type=msg_type, message=message)
|
||||
)
|
||||
return tuple(args), kwargs
|
||||
|
||||
def unregister_capability(
|
||||
self,
|
||||
params: UnregistrationParams,
|
||||
callback: Optional[Callable[[], None]] = None,
|
||||
) -> Future:
|
||||
"""Unregister a new capability on the client.
|
||||
|
||||
Args:
|
||||
params(UnregistrationParams): UnregistrationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(CLIENT_UNREGISTER_CAPABILITY, params, callback)
|
||||
def _get_handler_params_annotations(handler: Callable[..., Any]):
|
||||
"""Return the parameters and corresponding type annotations for the given handler
|
||||
function."""
|
||||
|
||||
def unregister_capability_async(
|
||||
self, params: UnregistrationParams
|
||||
) -> asyncio.Future:
|
||||
"""Unregister a new capability on the client.
|
||||
# If the user's handler requests the language server instance, the real function
|
||||
# is wrapped inside whatever `functools.partial()` returns.
|
||||
if hasattr(handler, "func"):
|
||||
annotations = typing.get_type_hints(handler.func)
|
||||
params = inspect.signature(handler.func).parameters
|
||||
|
||||
Args:
|
||||
params(UnregistrationParams): UnregistrationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.unregister_capability(params, None))
|
||||
else:
|
||||
annotations = typing.get_type_hints(handler)
|
||||
params = inspect.signature(handler).parameters
|
||||
|
||||
return params, annotations
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import functools
|
||||
import logging
|
||||
from pygls.constants import ATTR_FEATURE_TYPE
|
||||
from pygls.feature_manager import assign_help_attrs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def call_user_feature(base_func, method_name):
|
||||
"""Wraps generic LSP features and calls user registered feature
|
||||
immediately after it.
|
||||
"""
|
||||
|
||||
@functools.wraps(base_func)
|
||||
def decorator(self, *args, **kwargs):
|
||||
ret_val = base_func(self, *args, **kwargs)
|
||||
|
||||
try:
|
||||
user_func = self.fm.features[method_name]
|
||||
self._execute_notification(user_func, *args, **kwargs)
|
||||
except KeyError:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Failed to handle user defined notification "%s": %s', method_name, args
|
||||
)
|
||||
|
||||
return ret_val
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class LSPMeta(type):
|
||||
"""Wraps LSP built-in features (`lsp_` naming convention).
|
||||
|
||||
Built-in features cannot be overridden but user defined features with
|
||||
the same LSP name will be called after them.
|
||||
"""
|
||||
|
||||
def __new__(mcs, cls_name, cls_bases, cls):
|
||||
for attr_name, attr_val in cls.items():
|
||||
if callable(attr_val) and hasattr(attr_val, "method_name"):
|
||||
method_name = attr_val.method_name
|
||||
wrapped = call_user_feature(attr_val, method_name)
|
||||
assign_help_attrs(wrapped, method_name, ATTR_FEATURE_TYPE)
|
||||
cls[attr_name] = wrapped
|
||||
|
||||
logger.debug('Added decorator for lsp method: "%s"', attr_name)
|
||||
|
||||
return super().__new__(mcs, cls_name, cls_bases, cls)
|
||||
Reference in New Issue
Block a user