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:
@@ -21,5 +21,7 @@ import sys
|
||||
|
||||
IS_WIN = os.name == "nt"
|
||||
IS_PYODIDE = "pyodide" in sys.modules
|
||||
IS_WASI = sys.platform == "wasi"
|
||||
IS_WASM = IS_PYODIDE or IS_WASI
|
||||
|
||||
pygls = "pygls"
|
||||
|
||||
@@ -14,31 +14,23 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from functools import reduce
|
||||
from typing import Any, Dict, List, Optional, Set, Union, TypeVar
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Set, TypeVar, Union
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.lsp._capabilities import get_capability as get_capability
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def get_capability(
|
||||
client_capabilities: types.ClientCapabilities, field: str, default: Any = None
|
||||
) -> Any:
|
||||
"""Check if ClientCapabilities has some nested value without raising
|
||||
AttributeError.
|
||||
e.g. get_capability('text_document.synchronization.will_save')
|
||||
"""
|
||||
try:
|
||||
value = reduce(getattr, field.split("."), client_capabilities)
|
||||
except AttributeError:
|
||||
return default
|
||||
|
||||
# If we reach the desired leaf value but it's None, return the default.
|
||||
return default if value is None else value
|
||||
_SUPPORTED_ENCODINGS = frozenset(
|
||||
[
|
||||
types.PositionEncodingKind.Utf8,
|
||||
types.PositionEncodingKind.Utf16,
|
||||
types.PositionEncodingKind.Utf32,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ServerCapabilitiesBuilder:
|
||||
@@ -54,6 +46,9 @@ class ServerCapabilitiesBuilder:
|
||||
commands: List[str],
|
||||
text_document_sync_kind: types.TextDocumentSyncKind,
|
||||
notebook_document_sync: Optional[types.NotebookDocumentSyncOptions] = None,
|
||||
position_encoding: Union[
|
||||
types.PositionEncodingKind, str
|
||||
] = types.PositionEncodingKind.Utf16,
|
||||
):
|
||||
self.client_capabilities = client_capabilities
|
||||
self.features = features
|
||||
@@ -63,12 +58,37 @@ class ServerCapabilitiesBuilder:
|
||||
self.notebook_document_sync = notebook_document_sync
|
||||
|
||||
self.server_cap = types.ServerCapabilities()
|
||||
self.server_cap.position_encoding = position_encoding
|
||||
|
||||
def _provider_options(self, feature: str, default: T) -> Optional[Union[T, Any]]:
|
||||
if feature in self.features:
|
||||
return self.feature_options.get(feature, default)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def choose_position_encoding(
|
||||
cls, client_capabilities: types.ClientCapabilities
|
||||
) -> Union[types.PositionEncodingKind, str]:
|
||||
server_encoding: Union[types.PositionEncodingKind, str] = (
|
||||
types.PositionEncodingKind.Utf16
|
||||
)
|
||||
|
||||
if (general := client_capabilities.general) is None:
|
||||
return server_encoding
|
||||
|
||||
if (encodings := general.position_encodings) is None:
|
||||
return server_encoding
|
||||
|
||||
# We match client preference where this an overlap between its and our supported encodings.
|
||||
for client_encoding in encodings:
|
||||
if client_encoding in _SUPPORTED_ENCODINGS:
|
||||
server_encoding = client_encoding
|
||||
return server_encoding
|
||||
|
||||
logger.warning(f"Unknown `PositionEncoding`s: {encodings}")
|
||||
|
||||
return server_encoding
|
||||
|
||||
def _with_text_document_sync(self):
|
||||
open_close = (
|
||||
types.TEXT_DOCUMENT_DID_OPEN in self.features
|
||||
@@ -145,7 +165,7 @@ class ServerCapabilitiesBuilder:
|
||||
|
||||
def _with_type_definition(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_TYPE_DEFINITION, default=types.TypeDefinitionOptions()
|
||||
types.TEXT_DOCUMENT_TYPE_DEFINITION, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.type_definition_provider = value
|
||||
@@ -161,9 +181,7 @@ class ServerCapabilitiesBuilder:
|
||||
return self
|
||||
|
||||
def _with_implementation(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_IMPLEMENTATION, default=types.ImplementationOptions()
|
||||
)
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_IMPLEMENTATION, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.implementation_provider = value
|
||||
return self
|
||||
@@ -201,6 +219,7 @@ class ServerCapabilitiesBuilder:
|
||||
types.TEXT_DOCUMENT_CODE_LENS, default=types.CodeLensOptions()
|
||||
)
|
||||
if value is not None:
|
||||
value.resolve_provider = types.CODE_LENS_RESOLVE in self.features
|
||||
self.server_cap.code_lens_provider = value
|
||||
return self
|
||||
|
||||
@@ -209,6 +228,7 @@ class ServerCapabilitiesBuilder:
|
||||
types.TEXT_DOCUMENT_DOCUMENT_LINK, default=types.DocumentLinkOptions()
|
||||
)
|
||||
if value is not None:
|
||||
value.resolve_provider = types.DOCUMENT_LINK_RESOLVE in self.features
|
||||
self.server_cap.document_link_provider = value
|
||||
return self
|
||||
|
||||
@@ -241,9 +261,25 @@ class ServerCapabilitiesBuilder:
|
||||
return self
|
||||
|
||||
def _with_rename(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_RENAME, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.rename_provider = value
|
||||
server_supports_rename = types.TEXT_DOCUMENT_RENAME in self.features
|
||||
if server_supports_rename is False:
|
||||
return self
|
||||
|
||||
client_prepare_support = get_capability(
|
||||
self.client_capabilities, "text_document.rename.prepare_support", False
|
||||
)
|
||||
|
||||
# From the spec:
|
||||
# > RenameOptions may only be specified if the client states that it supports
|
||||
# > prepareSupport in its initial initialize request.
|
||||
if not client_prepare_support:
|
||||
self.server_cap.rename_provider = server_supports_rename
|
||||
|
||||
else:
|
||||
self.server_cap.rename_provider = types.RenameOptions(
|
||||
prepare_provider=types.TEXT_DOCUMENT_PREPARE_RENAME in self.features
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def _with_folding_range(self):
|
||||
@@ -302,12 +338,12 @@ class ServerCapabilitiesBuilder:
|
||||
self.server_cap.semantic_tokens_provider = value
|
||||
return self
|
||||
|
||||
full_support: Union[bool, types.SemanticTokensOptionsFullType1] = (
|
||||
full_support: Union[bool, types.SemanticTokensFullDelta] = (
|
||||
types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL in self.features
|
||||
)
|
||||
|
||||
if types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA in self.features:
|
||||
full_support = types.SemanticTokensOptionsFullType1(delta=True)
|
||||
full_support = types.SemanticTokensFullDelta(delta=True)
|
||||
|
||||
options = types.SemanticTokensOptions(
|
||||
legend=value,
|
||||
@@ -364,7 +400,7 @@ class ServerCapabilitiesBuilder:
|
||||
value = self._provider_options(method_name, default=None)
|
||||
setattr(file_operations, capability_name, value)
|
||||
|
||||
self.server_cap.workspace = types.ServerCapabilitiesWorkspaceType(
|
||||
self.server_cap.workspace = types.WorkspaceOptions(
|
||||
workspace_folders=types.WorkspaceFoldersServerCapabilities(
|
||||
supported=True,
|
||||
change_notifications=True,
|
||||
@@ -391,30 +427,12 @@ class ServerCapabilitiesBuilder:
|
||||
self.server_cap.inline_value_provider = value
|
||||
return self
|
||||
|
||||
def _with_position_encodings(self):
|
||||
self.server_cap.position_encoding = types.PositionEncodingKind.Utf16
|
||||
|
||||
general = self.client_capabilities.general
|
||||
if general is None:
|
||||
return self
|
||||
|
||||
encodings = general.position_encodings
|
||||
if encodings is None:
|
||||
return self
|
||||
|
||||
if types.PositionEncodingKind.Utf16 in encodings:
|
||||
return self
|
||||
|
||||
if types.PositionEncodingKind.Utf32 in encodings:
|
||||
self.server_cap.position_encoding = types.PositionEncodingKind.Utf32
|
||||
return self
|
||||
|
||||
if types.PositionEncodingKind.Utf8 in encodings:
|
||||
self.server_cap.position_encoding = types.PositionEncodingKind.Utf8
|
||||
return self
|
||||
|
||||
logger.warning(f"Unknown `PositionEncoding`s: {encodings}")
|
||||
|
||||
def _with_inline_completion_provider(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_INLINE_COMPLETION, default=None
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.inline_completion_provider = value
|
||||
return self
|
||||
|
||||
def _build(self):
|
||||
@@ -455,6 +473,6 @@ class ServerCapabilitiesBuilder:
|
||||
._with_workspace_capabilities()
|
||||
._with_diagnostic_provider()
|
||||
._with_inline_value_provider()
|
||||
._with_position_encodings()
|
||||
._with_inline_completion_provider()
|
||||
._build()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
############################################################################
|
||||
# Copyright(c) Open Law Library. All rights reserved. #
|
||||
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License") #
|
||||
# you may not use this file except in compliance with the License. #
|
||||
# You may obtain a copy of the License at #
|
||||
# #
|
||||
# http: // www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
"""A simple cli wrapper for pygls servers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from pygls.server import JsonRPCServer
|
||||
|
||||
|
||||
def start_server(server: JsonRPCServer, args: list[str] | None = None):
|
||||
"""A helper function that implements a simple cli wrapper for a pygls server
|
||||
allowing the user to select between the supported transports."""
|
||||
|
||||
name = type(server).__name__
|
||||
parser = argparse.ArgumentParser(description=f"start a {name} instance")
|
||||
parser.add_argument("--tcp", action="store_true", help="start a TCP server")
|
||||
parser.add_argument("--ws", action="store_true", help="start a WebSocket server")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="bind to this address")
|
||||
parser.add_argument("--port", type=int, default=8888, help="bind to this port")
|
||||
|
||||
arguments = parser.parse_args(args)
|
||||
|
||||
if arguments.tcp:
|
||||
server.start_tcp(arguments.host, arguments.port)
|
||||
elif arguments.ws:
|
||||
server.start_ws(arguments.host, arguments.port)
|
||||
else:
|
||||
server.start_io()
|
||||
+105
-66
@@ -14,63 +14,30 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import typing
|
||||
from threading import Event
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import Union
|
||||
|
||||
from cattrs import Converter
|
||||
|
||||
from pygls.exceptions import PyglsError, JsonRpcException
|
||||
from pygls.exceptions import JsonRpcException, PyglsError
|
||||
from pygls.io_ import run_async, run_websocket
|
||||
from pygls.protocol import JsonRPCProtocol, default_converter
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
from cattrs import Converter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def aio_readline(stop_event, reader, message_handler):
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
|
||||
# Initialize message buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = await reader.readline()
|
||||
if not header:
|
||||
break
|
||||
message.append(header)
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = await reader.readexactly(content_length)
|
||||
if not body:
|
||||
break
|
||||
message.append(body)
|
||||
|
||||
# Pass message to protocol
|
||||
message_handler(b"".join(message))
|
||||
|
||||
# Reset the buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
|
||||
class JsonRPCClient:
|
||||
"""Base JSON-RPC client."""
|
||||
|
||||
@@ -79,14 +46,14 @@ class JsonRPCClient:
|
||||
protocol_cls: Type[JsonRPCProtocol] = JsonRPCProtocol,
|
||||
converter_factory: Callable[[], Converter] = default_converter,
|
||||
):
|
||||
# Strictly speaking `JsonRPCProtocol` wants a `LanguageServer`, not a
|
||||
# `JsonRPCClient`. However there similar enough for our purposes, which is
|
||||
# that this client will mostly be used in testing contexts.
|
||||
# Strictly speaking, `JsonRPCProtocol` wants a `JsonRPCServer`, not a
|
||||
# `JsonRPCClient`. However they're similar enough for our purposes, which
|
||||
# is that this client will mostly be used in testing contexts.
|
||||
self.protocol = protocol_cls(self, converter_factory()) # type: ignore
|
||||
|
||||
self._server: Optional[asyncio.subprocess.Process] = None
|
||||
self._stop_event = Event()
|
||||
self._async_tasks: List[asyncio.Task] = []
|
||||
self._async_tasks: List[asyncio.Task[Any]] = []
|
||||
|
||||
@property
|
||||
def stopped(self) -> bool:
|
||||
@@ -128,39 +95,112 @@ class JsonRPCClient:
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.protocol.connection_made(server.stdin) # type: ignore
|
||||
# Keep mypy happy
|
||||
if server.stdout is None:
|
||||
raise RuntimeError("Server process is missing a stdout stream")
|
||||
|
||||
# Keep mypy happy
|
||||
if server.stdin is None:
|
||||
raise RuntimeError("Server process is missing a stdin stream")
|
||||
|
||||
self.protocol.set_writer(server.stdin)
|
||||
connection = asyncio.create_task(
|
||||
aio_readline(self._stop_event, server.stdout, self.protocol.data_received)
|
||||
run_async(
|
||||
stop_event=self._stop_event,
|
||||
reader=server.stdout,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
notify_exit = asyncio.create_task(self._server_exit())
|
||||
|
||||
self._server = server
|
||||
self._async_tasks.extend([connection, notify_exit])
|
||||
|
||||
async def _server_exit(self):
|
||||
if self._server is not None:
|
||||
await self._server.wait()
|
||||
logger.debug(
|
||||
"Server process %s exited with return code: %s",
|
||||
self._server.pid,
|
||||
self._server.returncode,
|
||||
async def start_tcp(self, host: str, port: int):
|
||||
"""Start communicating with a server over TCP."""
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
|
||||
self.protocol.set_writer(writer)
|
||||
connection = asyncio.create_task(
|
||||
run_async(
|
||||
stop_event=self._stop_event,
|
||||
reader=reader,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
|
||||
self._async_tasks.extend([connection])
|
||||
|
||||
async def start_ws(self, host: str, port: int):
|
||||
"""Start communicating with a server over WebSockets."""
|
||||
|
||||
try:
|
||||
from websockets.asyncio.client import connect
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
uri = f"ws://{host}:{port}"
|
||||
websocket = await connect(uri)
|
||||
connection = asyncio.create_task(
|
||||
run_websocket(
|
||||
stop_event=self._stop_event,
|
||||
websocket=websocket,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
self._async_tasks.extend([connection])
|
||||
|
||||
# Yield control to the event loop, gives the run_websocket task chance to spin up.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async def _server_exit(self):
|
||||
"""Cleanup handler that runs when the server process managed by the client exits"""
|
||||
if self._server is None:
|
||||
return
|
||||
|
||||
await self._server.wait()
|
||||
|
||||
pid = self._server.pid
|
||||
returncode = self._server.returncode
|
||||
|
||||
reason = f"Server process {pid} exited with return code: {returncode}"
|
||||
logger.debug(reason)
|
||||
|
||||
# Cancel any pending requests
|
||||
for id_, fut in self.protocol._request_futures.items():
|
||||
if not fut.done():
|
||||
fut.set_exception(RuntimeError(reason))
|
||||
logger.debug("Cancelled pending request '%s': %s", id_, reason)
|
||||
|
||||
try:
|
||||
await self.server_exit(self._server)
|
||||
self._stop_event.set()
|
||||
except Exception:
|
||||
logger.exception("Error in server_exit handler")
|
||||
|
||||
self._stop_event.set()
|
||||
|
||||
async def server_exit(self, server: asyncio.subprocess.Process):
|
||||
"""Called when the server process exits."""
|
||||
|
||||
def _report_server_error(
|
||||
self, error: Exception, source: Union[PyglsError, JsonRpcException]
|
||||
self, error: Exception, source: type[PyglsError] | type[JsonRpcException]
|
||||
):
|
||||
try:
|
||||
self.report_server_error(error, source)
|
||||
except Exception:
|
||||
logger.error("Unable to report error", exc_info=True)
|
||||
logger.exception("Unable to report error")
|
||||
|
||||
def report_server_error(
|
||||
self, error: Exception, source: Union[PyglsError, JsonRpcException]
|
||||
self, error: Exception, source: type[PyglsError] | type[JsonRpcException]
|
||||
):
|
||||
"""Called when the server does something unexpected e.g. respond with malformed
|
||||
JSON."""
|
||||
@@ -169,8 +209,7 @@ class JsonRPCClient:
|
||||
self._stop_event.set()
|
||||
|
||||
if self._server is not None and self._server.returncode is None:
|
||||
logger.debug("Terminating server process: %s", self._server.pid)
|
||||
self._server.terminate()
|
||||
await self._server.wait()
|
||||
|
||||
if len(self._async_tasks) > 0:
|
||||
await asyncio.gather(*self._async_tasks)
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from typing import Any
|
||||
from typing import Set
|
||||
from typing import Type
|
||||
from lsprotocol.types import ResponseError
|
||||
@@ -25,14 +28,23 @@ from lsprotocol.types import ResponseError
|
||||
class JsonRpcException(Exception):
|
||||
"""A class used as a base class for json rpc exceptions."""
|
||||
|
||||
def __init__(self, message=None, code=None, data=None):
|
||||
message = message or getattr(self.__class__, "MESSAGE")
|
||||
CODE = -32603
|
||||
MESSAGE = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str | None = None,
|
||||
code: int | None = None,
|
||||
data: Any | None = None,
|
||||
):
|
||||
message = message or self.MESSAGE
|
||||
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.code = code or getattr(self.__class__, "CODE")
|
||||
self.message: str = message
|
||||
self.code: int = code or self.CODE
|
||||
self.data = data
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any):
|
||||
return (
|
||||
isinstance(other, self.__class__)
|
||||
and self.code == other.code
|
||||
@@ -43,7 +55,7 @@ class JsonRpcException(Exception):
|
||||
return hash((self.code, self.message))
|
||||
|
||||
@staticmethod
|
||||
def from_error(error):
|
||||
def from_error(error: ResponseError):
|
||||
for exc_class in _EXCEPTIONS:
|
||||
if exc_class.supports_code(error.code):
|
||||
return exc_class(
|
||||
@@ -53,7 +65,16 @@ class JsonRpcException(Exception):
|
||||
return JsonRpcException(code=error.code, message=error.message, data=error.data)
|
||||
|
||||
@classmethod
|
||||
def supports_code(cls, code):
|
||||
def of(cls, exc: Any):
|
||||
"""Default ``of`` implementation that raises a ``JsonRpcException`` derived from
|
||||
the given exception
|
||||
"""
|
||||
return cls(
|
||||
message=f"{cls.MESSAGE}: {exc}",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def supports_code(cls, code: int):
|
||||
# Defaults to UnknownErrorCode
|
||||
return getattr(cls, "CODE", -32001) == code
|
||||
|
||||
@@ -91,7 +112,7 @@ class JsonRpcMethodNotFound(JsonRpcException):
|
||||
MESSAGE = "Method Not Found"
|
||||
|
||||
@classmethod
|
||||
def of(cls, method):
|
||||
def of(cls, method: str):
|
||||
return cls(message=cls.MESSAGE + ": " + method)
|
||||
|
||||
|
||||
|
||||
@@ -55,13 +55,21 @@ def get_help_attrs(f):
|
||||
)
|
||||
|
||||
|
||||
def has_ls_param_or_annotation(f, annotation):
|
||||
"""Returns true if callable has first parameter named `ls` or type of
|
||||
annotation"""
|
||||
def has_ls_param_or_annotation(f, actual_type):
|
||||
"""Returns true if the given callable's first parameter is
|
||||
|
||||
- named `ls`
|
||||
- has a type annotation compatible with the given type
|
||||
"""
|
||||
try:
|
||||
sig = inspect.signature(f)
|
||||
first_p = next(itertools.islice(sig.parameters.values(), 0, 1))
|
||||
return first_p.name == PARAM_LS or get_type_hints(f)[first_p.name] == annotation
|
||||
|
||||
if first_p.name == PARAM_LS:
|
||||
return True
|
||||
|
||||
expected_type = get_type_hints(f)[first_p.name]
|
||||
return issubclass(actual_type, expected_type)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -80,6 +88,10 @@ def wrap_with_server(f, server):
|
||||
async def wrapped(*args, **kwargs):
|
||||
return await f(server, *args, **kwargs)
|
||||
|
||||
# Used by `workspace/executeCommand` to access the original function's
|
||||
# signature. Mirrors how functools.partial works.
|
||||
wrapped.func = f # type: ignore[attr-defined]
|
||||
|
||||
else:
|
||||
wrapped = functools.partial(f, server)
|
||||
if is_thread_function(f):
|
||||
@@ -196,7 +208,8 @@ class FeatureManager:
|
||||
raise TypeError(
|
||||
(
|
||||
f'Options of method "{feature_name}"'
|
||||
f" should be instance of type {options_type}"
|
||||
f" is instance of type {type(options)}"
|
||||
f" which is not a subtype of {options_type}"
|
||||
)
|
||||
)
|
||||
self._feature_options[feature_name] = options
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
############################################################################
|
||||
# Copyright(c) Open Law Library. All rights reserved. #
|
||||
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License") #
|
||||
# you may not use this file except in compliance with the License. #
|
||||
# You may obtain a copy of the License at #
|
||||
# #
|
||||
# http: // www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
|
||||
from pygls.exceptions import JsonRpcException
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Awaitable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, BinaryIO, Callable, Protocol
|
||||
|
||||
from websockets.asyncio.client import ClientConnection
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
|
||||
from pygls.protocol import JsonRPCProtocol
|
||||
|
||||
class Reader(Protocol):
|
||||
"""An synchronous reader."""
|
||||
|
||||
def readline(self) -> bytes: ...
|
||||
|
||||
def read(self, n: int) -> bytes: ...
|
||||
|
||||
class Writer(Protocol):
|
||||
"""An synchronous writer."""
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
def write(self, data: bytes) -> None: ...
|
||||
|
||||
class AsyncReader(typing.Protocol):
|
||||
"""An asynchronous reader."""
|
||||
|
||||
def readline(self) -> Awaitable[bytes]: ...
|
||||
|
||||
def readexactly(self, n: int) -> Awaitable[bytes]: ...
|
||||
|
||||
class AsyncWriter(typing.Protocol):
|
||||
"""An asynchronous writer."""
|
||||
|
||||
def close(self) -> Awaitable[None]: ...
|
||||
|
||||
def write(self, data: bytes) -> Awaitable[None]: ...
|
||||
|
||||
|
||||
class StdinAsyncReader:
|
||||
"""Read from stdin asynchronously."""
|
||||
|
||||
def __init__(self, stdin: BinaryIO, executor: ThreadPoolExecutor | None = None):
|
||||
self.stdin = stdin
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
self.executor = executor
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
if self._loop is None:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
|
||||
return self._loop
|
||||
|
||||
def readline(self) -> Awaitable[bytes]:
|
||||
return self.loop.run_in_executor(self.executor, self.stdin.readline)
|
||||
|
||||
def readexactly(self, n: int) -> Awaitable[bytes]:
|
||||
return self.loop.run_in_executor(self.executor, self.stdin.read, n)
|
||||
|
||||
|
||||
class StdoutWriter:
|
||||
"""Align a stdout stream with pygls' writer interface."""
|
||||
|
||||
def __init__(self, stdout: BinaryIO):
|
||||
self._stdout = stdout
|
||||
|
||||
def close(self):
|
||||
self._stdout.close()
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
self._stdout.write(data)
|
||||
self._stdout.flush()
|
||||
|
||||
|
||||
class WebSocketWriter:
|
||||
"""Align a websocket connection with pygls' writer interface"""
|
||||
|
||||
def __init__(self, ws: ServerConnection | ClientConnection):
|
||||
self._ws = ws
|
||||
|
||||
def close(self) -> Awaitable[None]:
|
||||
return self._ws.close()
|
||||
|
||||
def write(self, data: bytes) -> Awaitable[None]:
|
||||
return self._ws.send(data)
|
||||
|
||||
|
||||
async def run_async(
|
||||
stop_event: threading.Event,
|
||||
reader: AsyncReader,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run a main message processing loop, asynchronously
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
reader
|
||||
The reader to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
"""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
content_length = 0
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = await reader.readline()
|
||||
if not header:
|
||||
break
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = await reader.readexactly(content_length)
|
||||
if not body:
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(body, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
finally:
|
||||
# Reset
|
||||
content_length = 0
|
||||
|
||||
|
||||
def run(
|
||||
stop_event: threading.Event,
|
||||
reader: Reader,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run a main message processing loop, synchronously
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
reader
|
||||
The reader to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
|
||||
error_handler
|
||||
Function to call when an error is encountered.
|
||||
"""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
content_length = 0
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = reader.readline()
|
||||
if not header:
|
||||
break
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = reader.read(content_length)
|
||||
if not body:
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(body, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
finally:
|
||||
# Reset
|
||||
content_length = 0
|
||||
|
||||
|
||||
async def run_websocket(
|
||||
websocket: ClientConnection | ServerConnection,
|
||||
stop_event: threading.Event,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run the main message processing loop, over websockets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
websocket
|
||||
The websocket to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
|
||||
error_handler
|
||||
Function to call when an error is encountered.
|
||||
"""
|
||||
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
protocol.set_writer(WebSocketWriter(websocket), include_headers=False)
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
||||
)
|
||||
return
|
||||
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
logger.debug("waiting for a message...")
|
||||
data = await websocket.recv(decode=False)
|
||||
except ConnectionClosed:
|
||||
logger.debug("Websocket connection closed.")
|
||||
stop_event.set()
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(data, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
|
||||
logger.debug("Exiting main loop")
|
||||
await websocket.close()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,463 @@
|
||||
############################################################################
|
||||
# Copyright(c) Open Law Library. All rights reserved. #
|
||||
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License") #
|
||||
# you may not use this file except in compliance with the License. #
|
||||
# You may obtain a copy of the License at #
|
||||
# #
|
||||
# http: // www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
|
||||
# GENERATED FROM scripts/generate_code.py -- DO NOT EDIT
|
||||
# flake8: noqa
|
||||
from __future__ import annotations
|
||||
|
||||
from lsprotocol import types
|
||||
from pygls.protocol import LanguageServerProtocol
|
||||
from pygls.protocol import default_converter
|
||||
from pygls.server import JsonRPCServer
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from cattrs import Converter
|
||||
from concurrent.futures import Future
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
class BaseLanguageServer(JsonRPCServer):
|
||||
|
||||
protocol: LanguageServerProtocol
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protocol_cls: type[LanguageServerProtocol] = LanguageServerProtocol,
|
||||
converter_factory: Callable[[], Converter] = default_converter,
|
||||
max_workers: int | None = None,
|
||||
):
|
||||
super().__init__(protocol_cls, converter_factory, max_workers)
|
||||
|
||||
def client_register_capability(
|
||||
self,
|
||||
params: types.RegistrationParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`client/registerCapability` request.
|
||||
|
||||
The `client/registerCapability` request is sent from the server to the client to register a new capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return self.protocol.send_request("client/registerCapability", params, callback)
|
||||
|
||||
async def client_register_capability_async(
|
||||
self,
|
||||
params: types.RegistrationParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`client/registerCapability` request.
|
||||
|
||||
The `client/registerCapability` request is sent from the server to the client to register a new capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return await self.protocol.send_request_async("client/registerCapability", params)
|
||||
|
||||
def client_unregister_capability(
|
||||
self,
|
||||
params: types.UnregistrationParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`client/unregisterCapability` request.
|
||||
|
||||
The `client/unregisterCapability` request is sent from the server to the client to unregister a previously registered capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return self.protocol.send_request("client/unregisterCapability", params, callback)
|
||||
|
||||
async def client_unregister_capability_async(
|
||||
self,
|
||||
params: types.UnregistrationParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`client/unregisterCapability` request.
|
||||
|
||||
The `client/unregisterCapability` request is sent from the server to the client to unregister a previously registered capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return await self.protocol.send_request_async("client/unregisterCapability", params)
|
||||
|
||||
def window_show_document(
|
||||
self,
|
||||
params: types.ShowDocumentParams,
|
||||
callback: Optional[Callable[[types.ShowDocumentResult], None]] = None,
|
||||
) -> Future[types.ShowDocumentResult]:
|
||||
"""Make a :lsp:`window/showDocument` request.
|
||||
|
||||
A request to show a document. This request might open an
|
||||
external program depending on the value of the URI to open.
|
||||
For example a request to open `https://code.visualstudio.com/`
|
||||
will very likely open the URI in a WEB browser.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return self.protocol.send_request("window/showDocument", params, callback)
|
||||
|
||||
async def window_show_document_async(
|
||||
self,
|
||||
params: types.ShowDocumentParams,
|
||||
) -> types.ShowDocumentResult:
|
||||
"""Make a :lsp:`window/showDocument` request.
|
||||
|
||||
A request to show a document. This request might open an
|
||||
external program depending on the value of the URI to open.
|
||||
For example a request to open `https://code.visualstudio.com/`
|
||||
will very likely open the URI in a WEB browser.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("window/showDocument", params)
|
||||
|
||||
def window_show_message_request(
|
||||
self,
|
||||
params: types.ShowMessageRequestParams,
|
||||
callback: Optional[Callable[[Optional[types.MessageActionItem]], None]] = None,
|
||||
) -> Future[Optional[types.MessageActionItem]]:
|
||||
"""Make a :lsp:`window/showMessageRequest` request.
|
||||
|
||||
The show message request is sent from the server to the client to show a message
|
||||
and a set of options actions to the user.
|
||||
"""
|
||||
return self.protocol.send_request("window/showMessageRequest", params, callback)
|
||||
|
||||
async def window_show_message_request_async(
|
||||
self,
|
||||
params: types.ShowMessageRequestParams,
|
||||
) -> Optional[types.MessageActionItem]:
|
||||
"""Make a :lsp:`window/showMessageRequest` request.
|
||||
|
||||
The show message request is sent from the server to the client to show a message
|
||||
and a set of options actions to the user.
|
||||
"""
|
||||
return await self.protocol.send_request_async("window/showMessageRequest", params)
|
||||
|
||||
def window_work_done_progress_create(
|
||||
self,
|
||||
params: types.WorkDoneProgressCreateParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`window/workDoneProgress/create` request.
|
||||
|
||||
The `window/workDoneProgress/create` request is sent from the server to the client to initiate progress
|
||||
reporting from the server.
|
||||
"""
|
||||
return self.protocol.send_request("window/workDoneProgress/create", params, callback)
|
||||
|
||||
async def window_work_done_progress_create_async(
|
||||
self,
|
||||
params: types.WorkDoneProgressCreateParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`window/workDoneProgress/create` request.
|
||||
|
||||
The `window/workDoneProgress/create` request is sent from the server to the client to initiate progress
|
||||
reporting from the server.
|
||||
"""
|
||||
return await self.protocol.send_request_async("window/workDoneProgress/create", params)
|
||||
|
||||
def workspace_apply_edit(
|
||||
self,
|
||||
params: types.ApplyWorkspaceEditParams,
|
||||
callback: Optional[Callable[[types.ApplyWorkspaceEditResult], None]] = None,
|
||||
) -> Future[types.ApplyWorkspaceEditResult]:
|
||||
"""Make a :lsp:`workspace/applyEdit` request.
|
||||
|
||||
A request sent from the server to the client to modified certain resources.
|
||||
"""
|
||||
return self.protocol.send_request("workspace/applyEdit", params, callback)
|
||||
|
||||
async def workspace_apply_edit_async(
|
||||
self,
|
||||
params: types.ApplyWorkspaceEditParams,
|
||||
) -> types.ApplyWorkspaceEditResult:
|
||||
"""Make a :lsp:`workspace/applyEdit` request.
|
||||
|
||||
A request sent from the server to the client to modified certain resources.
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/applyEdit", params)
|
||||
|
||||
def workspace_code_lens_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/codeLens/refresh` request.
|
||||
|
||||
A request to refresh all code actions
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/codeLens/refresh", params, callback)
|
||||
|
||||
async def workspace_code_lens_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/codeLens/refresh` request.
|
||||
|
||||
A request to refresh all code actions
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/codeLens/refresh", params)
|
||||
|
||||
def workspace_configuration(
|
||||
self,
|
||||
params: types.ConfigurationParams,
|
||||
callback: Optional[Callable[[Sequence[Optional[Any]]], None]] = None,
|
||||
) -> Future[Sequence[Optional[Any]]]:
|
||||
"""Make a :lsp:`workspace/configuration` request.
|
||||
|
||||
The 'workspace/configuration' request is sent from the server to the client to fetch a certain
|
||||
configuration setting.
|
||||
|
||||
This pull model replaces the old push model were the client signaled configuration change via an
|
||||
event. If the server still needs to react to configuration changes (since the server caches the
|
||||
result of `workspace/configuration` requests) the server should register for an empty configuration
|
||||
change event and empty the cache if such an event is received.
|
||||
"""
|
||||
return self.protocol.send_request("workspace/configuration", params, callback)
|
||||
|
||||
async def workspace_configuration_async(
|
||||
self,
|
||||
params: types.ConfigurationParams,
|
||||
) -> Sequence[Optional[Any]]:
|
||||
"""Make a :lsp:`workspace/configuration` request.
|
||||
|
||||
The 'workspace/configuration' request is sent from the server to the client to fetch a certain
|
||||
configuration setting.
|
||||
|
||||
This pull model replaces the old push model were the client signaled configuration change via an
|
||||
event. If the server still needs to react to configuration changes (since the server caches the
|
||||
result of `workspace/configuration` requests) the server should register for an empty configuration
|
||||
change event and empty the cache if such an event is received.
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/configuration", params)
|
||||
|
||||
def workspace_diagnostic_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/diagnostic/refresh` request.
|
||||
|
||||
The diagnostic refresh request definition.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/diagnostic/refresh", params, callback)
|
||||
|
||||
async def workspace_diagnostic_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/diagnostic/refresh` request.
|
||||
|
||||
The diagnostic refresh request definition.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/diagnostic/refresh", params)
|
||||
|
||||
def workspace_folding_range_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/foldingRange/refresh` request.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return self.protocol.send_request("workspace/foldingRange/refresh", params, callback)
|
||||
|
||||
async def workspace_folding_range_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/foldingRange/refresh` request.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/foldingRange/refresh", params)
|
||||
|
||||
def workspace_inlay_hint_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/inlayHint/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/inlayHint/refresh", params, callback)
|
||||
|
||||
async def workspace_inlay_hint_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/inlayHint/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/inlayHint/refresh", params)
|
||||
|
||||
def workspace_inline_value_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/inlineValue/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/inlineValue/refresh", params, callback)
|
||||
|
||||
async def workspace_inline_value_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/inlineValue/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/inlineValue/refresh", params)
|
||||
|
||||
def workspace_semantic_tokens_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/semanticTokens/refresh` request.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/semanticTokens/refresh", params, callback)
|
||||
|
||||
async def workspace_semantic_tokens_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/semanticTokens/refresh` request.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/semanticTokens/refresh", params)
|
||||
|
||||
def workspace_text_document_content_refresh(
|
||||
self,
|
||||
params: types.TextDocumentContentRefreshParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/textDocumentContent/refresh` request.
|
||||
|
||||
The `workspace/textDocumentContent` request is sent from the server to the client to refresh
|
||||
the content of a specific text document.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return self.protocol.send_request("workspace/textDocumentContent/refresh", params, callback)
|
||||
|
||||
async def workspace_text_document_content_refresh_async(
|
||||
self,
|
||||
params: types.TextDocumentContentRefreshParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/textDocumentContent/refresh` request.
|
||||
|
||||
The `workspace/textDocumentContent` request is sent from the server to the client to refresh
|
||||
the content of a specific text document.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/textDocumentContent/refresh", params)
|
||||
|
||||
def workspace_workspace_folders(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[Optional[Sequence[types.WorkspaceFolder]]], None]] = None,
|
||||
) -> Future[Optional[Sequence[types.WorkspaceFolder]]]:
|
||||
"""Make a :lsp:`workspace/workspaceFolders` request.
|
||||
|
||||
The `workspace/workspaceFolders` is sent from the server to the client to fetch the open workspace folders.
|
||||
"""
|
||||
return self.protocol.send_request("workspace/workspaceFolders", params, callback)
|
||||
|
||||
async def workspace_workspace_folders_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> Optional[Sequence[types.WorkspaceFolder]]:
|
||||
"""Make a :lsp:`workspace/workspaceFolders` request.
|
||||
|
||||
The `workspace/workspaceFolders` is sent from the server to the client to fetch the open workspace folders.
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/workspaceFolders", params)
|
||||
|
||||
def cancel_request(self, params: types.CancelParams) -> None:
|
||||
"""Send a :lsp:`$/cancelRequest` notification.
|
||||
|
||||
|
||||
"""
|
||||
self.protocol.notify("$/cancelRequest", params)
|
||||
|
||||
def log_trace(self, params: types.LogTraceParams) -> None:
|
||||
"""Send a :lsp:`$/logTrace` notification.
|
||||
|
||||
|
||||
"""
|
||||
self.protocol.notify("$/logTrace", params)
|
||||
|
||||
def progress(self, params: types.ProgressParams) -> None:
|
||||
"""Send a :lsp:`$/progress` notification.
|
||||
|
||||
|
||||
"""
|
||||
self.protocol.notify("$/progress", params)
|
||||
|
||||
def telemetry_event(self, params: typing.Optional[typing.Any]) -> None:
|
||||
"""Send a :lsp:`telemetry/event` notification.
|
||||
|
||||
The telemetry event notification is sent from the server to the client to ask
|
||||
the client to log telemetry data.
|
||||
"""
|
||||
self.protocol.notify("telemetry/event", params)
|
||||
|
||||
def text_document_publish_diagnostics(self, params: types.PublishDiagnosticsParams) -> None:
|
||||
"""Send a :lsp:`textDocument/publishDiagnostics` notification.
|
||||
|
||||
Diagnostics notification are sent from the server to the client to signal
|
||||
results of validation runs.
|
||||
"""
|
||||
self.protocol.notify("textDocument/publishDiagnostics", params)
|
||||
|
||||
def window_log_message(self, params: types.LogMessageParams) -> None:
|
||||
"""Send a :lsp:`window/logMessage` notification.
|
||||
|
||||
The log message notification is sent from the server to the client to ask
|
||||
the client to log a particular message.
|
||||
"""
|
||||
self.protocol.notify("window/logMessage", params)
|
||||
|
||||
def window_show_message(self, params: types.ShowMessageParams) -> None:
|
||||
"""Send a :lsp:`window/showMessage` notification.
|
||||
|
||||
The show message notification is sent from a server to a client to ask
|
||||
the client to display a particular message in the user interface.
|
||||
"""
|
||||
self.protocol.notify("window/showMessage", params)
|
||||
File diff suppressed because it is too large
Load Diff
+4
-1959
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.exceptions import FeatureRequestError
|
||||
|
||||
from ._base_server import BaseLanguageServer
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from typing import TypeVar
|
||||
|
||||
from pygls.server import ServerErrors
|
||||
from pygls.progress import Progress
|
||||
from pygls.workspace import Workspace
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
|
||||
class LanguageServer(BaseLanguageServer):
|
||||
"""The default LanguageServer
|
||||
|
||||
This class can be extended and it can be passed as a first argument to
|
||||
registered commands/features.
|
||||
|
||||
.. |ServerInfo| replace:: :class:`~lsprotocol.types.ServerInfo`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name
|
||||
Name of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
version
|
||||
Version of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
protocol_cls
|
||||
The :class:`~pygls.protocol.LanguageServerProtocol` class definition, or any
|
||||
subclass of it.
|
||||
|
||||
max_workers
|
||||
Maximum number of workers for ``ThreadPool`` and ``ThreadPoolExecutor``
|
||||
|
||||
text_document_sync_kind
|
||||
Text document synchronization method
|
||||
|
||||
None
|
||||
No synchronization
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Full`
|
||||
Send entire document text with each update
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`
|
||||
Send only the region of text that changed with each update
|
||||
|
||||
notebook_document_sync
|
||||
Advertise :lsp:`NotebookDocument` support to the client.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str,
|
||||
text_document_sync_kind: types.TextDocumentSyncKind = types.TextDocumentSyncKind.Incremental,
|
||||
notebook_document_sync: types.NotebookDocumentSyncOptions | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self._text_document_sync_kind = text_document_sync_kind
|
||||
self._notebook_document_sync = notebook_document_sync
|
||||
self.process_id: int | None = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def client_capabilities(self) -> types.ClientCapabilities:
|
||||
"""The client's capabilities."""
|
||||
return self.protocol.client_capabilities
|
||||
|
||||
@property
|
||||
def server_capabilities(self) -> types.ServerCapabilities:
|
||||
"""The server's capabilities."""
|
||||
return self.protocol.server_capabilities
|
||||
|
||||
@property
|
||||
def workspace(self) -> Workspace:
|
||||
"""Returns in-memory workspace."""
|
||||
return self.protocol.workspace
|
||||
|
||||
@property
|
||||
def work_done_progress(self) -> Progress:
|
||||
"""Gets the object to manage client's progress bar."""
|
||||
return self.protocol.progress
|
||||
|
||||
def report_server_error(self, error: Exception, source: ServerErrors):
|
||||
"""
|
||||
Sends error to the client for displaying.
|
||||
|
||||
By default this function does not handle LSP request errors. This is because LSP requests
|
||||
require direct responses and so already have a mechanism for including unexpected errors
|
||||
in the response body.
|
||||
|
||||
All other errors are "out of band" in the sense that the client isn't explicitly waiting
|
||||
for them. For example diagnostics are returned as notifications, not responses to requests,
|
||||
and so can seemingly be sent at random. Also for example consider JSON RPC serialization
|
||||
and deserialization, if a payload cannot be parsed then the whole request/response cycle
|
||||
cannot be completed and so one of these "out of band" error messages is sent.
|
||||
|
||||
These "out of band" error messages are not a requirement of the LSP spec. Pygls simply
|
||||
offers this behaviour as a recommended default. It is perfectly reasonble to override this
|
||||
default.
|
||||
"""
|
||||
|
||||
if source == FeatureRequestError:
|
||||
return
|
||||
|
||||
self.window_show_message(
|
||||
types.ShowMessageParams(
|
||||
message=f"Error in server: {error}",
|
||||
type=types.MessageType.Error,
|
||||
)
|
||||
)
|
||||
@@ -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)
|
||||
+156
-489
@@ -14,213 +14,66 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import typing
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Event
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
TextIO,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import cattrs
|
||||
from pygls import IS_PYODIDE
|
||||
from pygls.lsp import ConfigCallbackType, ShowDocumentCallbackType
|
||||
from pygls.exceptions import (
|
||||
FeatureNotificationError,
|
||||
JsonRpcInternalError,
|
||||
PyglsError,
|
||||
JsonRpcException,
|
||||
FeatureRequestError,
|
||||
)
|
||||
from lsprotocol.types import (
|
||||
ClientCapabilities,
|
||||
Diagnostic,
|
||||
MessageType,
|
||||
NotebookDocumentSyncOptions,
|
||||
RegistrationParams,
|
||||
ServerCapabilities,
|
||||
ShowDocumentParams,
|
||||
TextDocumentSyncKind,
|
||||
UnregistrationParams,
|
||||
WorkspaceApplyEditResponse,
|
||||
WorkspaceEdit,
|
||||
WorkspaceConfigurationParams,
|
||||
)
|
||||
from pygls.progress import Progress
|
||||
from pygls.protocol import JsonRPCProtocol, LanguageServerProtocol, default_converter
|
||||
from pygls.workspace import Workspace
|
||||
|
||||
if not IS_PYODIDE:
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from pygls import IS_WASM
|
||||
from pygls.exceptions import JsonRpcException, PyglsError
|
||||
from pygls.io_ import StdinAsyncReader, StdoutWriter, run, run_async, run_websocket
|
||||
from pygls.protocol import JsonRPCProtocol
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any, BinaryIO, Callable, Optional, Type, TypeVar, Union
|
||||
|
||||
from websockets.asyncio.server import Server as WSServer
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
ServerErrors = Union[type[PyglsError], type[JsonRpcException]]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
ServerErrors = Union[
|
||||
PyglsError,
|
||||
JsonRpcException,
|
||||
Type[JsonRpcInternalError],
|
||||
Type[FeatureNotificationError],
|
||||
Type[FeatureRequestError],
|
||||
]
|
||||
|
||||
|
||||
async def aio_readline(loop, executor, stop_event, rfile, proxy):
|
||||
"""Reads data from stdin in separate thread (asynchronously)."""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
|
||||
# Initialize message buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
while not stop_event.is_set() and not rfile.closed:
|
||||
# Read a header line
|
||||
header = await loop.run_in_executor(executor, rfile.readline)
|
||||
if not header:
|
||||
break
|
||||
message.append(header)
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = await loop.run_in_executor(executor, rfile.read, content_length)
|
||||
if not body:
|
||||
break
|
||||
message.append(body)
|
||||
|
||||
# Pass message to language server protocol
|
||||
proxy(b"".join(message))
|
||||
|
||||
# Reset the buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
|
||||
class StdOutTransportAdapter:
|
||||
"""Protocol adapter which overrides write method.
|
||||
|
||||
Write method sends data to stdout.
|
||||
"""
|
||||
|
||||
def __init__(self, rfile, wfile):
|
||||
self.rfile = rfile
|
||||
self.wfile = wfile
|
||||
|
||||
def close(self):
|
||||
self.rfile.close()
|
||||
self.wfile.close()
|
||||
|
||||
def write(self, data):
|
||||
self.wfile.write(data)
|
||||
self.wfile.flush()
|
||||
|
||||
|
||||
class PyodideTransportAdapter:
|
||||
"""Protocol adapter which overrides write method.
|
||||
|
||||
Write method sends data to stdout.
|
||||
"""
|
||||
|
||||
def __init__(self, wfile):
|
||||
self.wfile = wfile
|
||||
|
||||
def close(self):
|
||||
self.wfile.close()
|
||||
|
||||
def write(self, data):
|
||||
self.wfile.write(data)
|
||||
self.wfile.flush()
|
||||
|
||||
|
||||
class WebSocketTransportAdapter:
|
||||
"""Protocol adapter which calls write method.
|
||||
|
||||
Write method sends data via the WebSocket interface.
|
||||
"""
|
||||
|
||||
def __init__(self, ws, loop):
|
||||
self._ws = ws
|
||||
self._loop = loop
|
||||
|
||||
def close(self) -> None:
|
||||
"""Stop the WebSocket server."""
|
||||
self._ws.close()
|
||||
|
||||
def write(self, data: Any) -> None:
|
||||
"""Create a task to write specified data into a WebSocket."""
|
||||
asyncio.ensure_future(self._ws.send(data))
|
||||
|
||||
|
||||
class Server:
|
||||
class JsonRPCServer:
|
||||
"""Base server class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
protocol_cls
|
||||
Protocol implementation that must be derive from :class:`~pygls.protocol.JsonRPCProtocol`
|
||||
Protocol implementation that should derive from
|
||||
:class:`~pygls.protocol.JsonRPCProtocol`
|
||||
|
||||
converter_factory
|
||||
Factory function to use when constructing a cattrs converter.
|
||||
|
||||
loop
|
||||
The asyncio event loop
|
||||
|
||||
max_workers
|
||||
Maximum number of workers for `ThreadPool` and `ThreadPoolExecutor`
|
||||
Maximum number of workers for `ThreadPoolExecutor`
|
||||
|
||||
"""
|
||||
|
||||
protocol: JsonRPCProtocol
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protocol_cls: Type[JsonRPCProtocol],
|
||||
converter_factory: Callable[[], cattrs.Converter],
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
max_workers: int = 2,
|
||||
sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
max_workers: int | None = None,
|
||||
):
|
||||
if not issubclass(protocol_cls, asyncio.Protocol):
|
||||
raise TypeError("Protocol class should be subclass of asyncio.Protocol")
|
||||
|
||||
self._max_workers = max_workers
|
||||
self._server = None
|
||||
self._stop_event: Optional[Event] = None
|
||||
self._thread_pool: Optional[ThreadPool] = None
|
||||
self._thread_pool_executor: Optional[ThreadPoolExecutor] = None
|
||||
self._server: asyncio.Server | WSServer | None = None
|
||||
self._stop_event: Event | None = None
|
||||
self._thread_pool: ThreadPoolExecutor | None = None
|
||||
|
||||
if sync_kind is not None:
|
||||
self.text_document_sync_kind = sync_kind
|
||||
|
||||
if loop is None:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self._owns_loop = True
|
||||
else:
|
||||
self._owns_loop = False
|
||||
|
||||
self.loop = loop
|
||||
|
||||
# TODO: Will move this to `LanguageServer` soon
|
||||
self.lsp = protocol_cls(self, converter_factory()) # type: ignore
|
||||
self.protocol = protocol_cls(self, converter_factory())
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown server."""
|
||||
@@ -230,38 +83,55 @@ class Server:
|
||||
self._stop_event.set()
|
||||
|
||||
if self._thread_pool:
|
||||
self._thread_pool.terminate()
|
||||
self._thread_pool.join()
|
||||
|
||||
if self._thread_pool_executor:
|
||||
self._thread_pool_executor.shutdown()
|
||||
self._thread_pool.shutdown()
|
||||
|
||||
if self._server:
|
||||
self._server.close()
|
||||
self.loop.run_until_complete(self._server.wait_closed())
|
||||
|
||||
if self._owns_loop and not self.loop.is_closed():
|
||||
logger.info("Closing the event loop.")
|
||||
self.loop.close()
|
||||
def _report_server_error(
|
||||
self,
|
||||
error: Exception,
|
||||
source: ServerErrors,
|
||||
):
|
||||
# Prevent recursive error reporting
|
||||
try:
|
||||
self.report_server_error(error, source)
|
||||
except Exception:
|
||||
logger.warning("Failed to report error")
|
||||
|
||||
def start_io(self, stdin: Optional[TextIO] = None, stdout: Optional[TextIO] = None):
|
||||
"""Starts IO server."""
|
||||
logger.info("Starting IO server")
|
||||
def report_server_error(self, error: Exception, source: ServerErrors):
|
||||
"""Default error reporter."""
|
||||
logger.error("%s", error)
|
||||
|
||||
def start_io(
|
||||
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
||||
):
|
||||
"""Starts an IO server."""
|
||||
|
||||
if IS_WASM:
|
||||
self._start_io_sync(stdin, stdout)
|
||||
else:
|
||||
self._start_io_async(stdin, stdout)
|
||||
|
||||
def _start_io_async(
|
||||
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
||||
):
|
||||
"""Starts an asynchronous IO server."""
|
||||
logger.info("Starting async IO server")
|
||||
|
||||
self._stop_event = Event()
|
||||
transport = StdOutTransportAdapter(
|
||||
stdin or sys.stdin.buffer, stdout or sys.stdout.buffer
|
||||
)
|
||||
self.lsp.connection_made(transport) # type: ignore[arg-type]
|
||||
reader = StdinAsyncReader(stdin or sys.stdin.buffer, self.thread_pool)
|
||||
writer = StdoutWriter(stdout or sys.stdout.buffer)
|
||||
self.protocol.set_writer(writer)
|
||||
|
||||
try:
|
||||
self.loop.run_until_complete(
|
||||
aio_readline(
|
||||
self.loop,
|
||||
self.thread_pool_executor,
|
||||
self._stop_event,
|
||||
stdin or sys.stdin.buffer,
|
||||
self.lsp.data_received,
|
||||
asyncio.run(
|
||||
run_async(
|
||||
stop_event=self._stop_event,
|
||||
reader=reader,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
except BrokenPipeError:
|
||||
@@ -271,169 +141,104 @@ class Server:
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
def start_pyodide(self):
|
||||
logger.info("Starting Pyodide server")
|
||||
def _start_io_sync(
|
||||
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
||||
):
|
||||
"""Starts an synchronous IO server."""
|
||||
logger.info("Starting sync IO server")
|
||||
|
||||
# Note: We don't actually start anything running as the main event
|
||||
# loop will be handled by the web platform.
|
||||
transport = PyodideTransportAdapter(sys.stdout)
|
||||
self.lsp.connection_made(transport) # type: ignore[arg-type]
|
||||
self.lsp._send_only_body = True # Don't send headers within the payload
|
||||
self._stop_event = Event()
|
||||
writer = StdoutWriter(stdout or sys.stdout.buffer)
|
||||
self.protocol.set_writer(writer)
|
||||
|
||||
try:
|
||||
asyncio.run(
|
||||
run(
|
||||
stop_event=self._stop_event,
|
||||
reader=stdin or sys.stdin.buffer,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
except BrokenPipeError:
|
||||
logger.error("Connection to the client is lost! Shutting down the server.")
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
def start_tcp(self, host: str, port: int) -> None:
|
||||
"""Starts TCP server."""
|
||||
logger.info("Starting TCP server on %s:%s", host, port)
|
||||
|
||||
self._stop_event = Event()
|
||||
self._server = self.loop.run_until_complete( # type: ignore[assignment]
|
||||
self.loop.create_server(self.lsp, host, port)
|
||||
)
|
||||
try:
|
||||
self.loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
self._stop_event = stop_event = Event()
|
||||
|
||||
async def lsp_connection(
|
||||
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
):
|
||||
logger.debug("Connected to client")
|
||||
self.protocol.set_writer(writer) # type: ignore
|
||||
await run_async(
|
||||
stop_event=stop_event,
|
||||
reader=reader,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
logger.debug("Main loop finished")
|
||||
self.shutdown()
|
||||
|
||||
async def tcp_server(h: str, p: int):
|
||||
self._server = await asyncio.start_server(lsp_connection, h, p)
|
||||
|
||||
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
|
||||
logger.info(f"Serving on {addrs}")
|
||||
|
||||
async with self._server:
|
||||
await self._server.serve_forever()
|
||||
|
||||
try:
|
||||
asyncio.run(tcp_server(host, port))
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Server was cancelled")
|
||||
|
||||
def start_ws(self, host: str, port: int) -> None:
|
||||
"""Starts WebSocket server."""
|
||||
try:
|
||||
from websockets.server import serve
|
||||
from websockets.asyncio.server import serve
|
||||
except ImportError:
|
||||
logger.error("Run `pip install pygls[ws]` to install `websockets`.")
|
||||
logger.error(
|
||||
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Starting WebSocket server on {}:{}".format(host, port))
|
||||
self._stop_event = stop_event = Event()
|
||||
|
||||
self._stop_event = Event()
|
||||
self.lsp._send_only_body = True # Don't send headers within the payload
|
||||
|
||||
async def connection_made(websocket, _):
|
||||
"""Handle new connection wrapped in the WebSocket."""
|
||||
self.lsp.transport = WebSocketTransportAdapter(websocket, self.loop)
|
||||
async for message in websocket:
|
||||
self.lsp._procedure_handler(
|
||||
json.loads(message, object_hook=self.lsp._deserialize_message)
|
||||
)
|
||||
|
||||
start_server = serve(connection_made, host, port, loop=self.loop)
|
||||
self._server = start_server.ws_server # type: ignore[assignment]
|
||||
self.loop.run_until_complete(start_server)
|
||||
|
||||
try:
|
||||
self.loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
self._stop_event.set()
|
||||
async def lsp_connection(websocket: ServerConnection):
|
||||
await run_websocket(
|
||||
stop_event=stop_event,
|
||||
websocket=websocket,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
self.shutdown()
|
||||
|
||||
if not IS_PYODIDE:
|
||||
async def ws_server(h: str, p: int):
|
||||
self._server = await serve(lsp_connection, host, port)
|
||||
|
||||
@property
|
||||
def thread_pool(self) -> ThreadPool:
|
||||
"""Returns thread pool instance (lazy initialization)."""
|
||||
if not self._thread_pool:
|
||||
self._thread_pool = ThreadPool(processes=self._max_workers)
|
||||
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
|
||||
logger.info(f"Serving on {addrs}")
|
||||
|
||||
return self._thread_pool
|
||||
async with self._server:
|
||||
await self._server.serve_forever()
|
||||
|
||||
@property
|
||||
def thread_pool_executor(self) -> ThreadPoolExecutor:
|
||||
"""Returns thread pool instance (lazy initialization)."""
|
||||
if not self._thread_pool_executor:
|
||||
self._thread_pool_executor = ThreadPoolExecutor(
|
||||
max_workers=self._max_workers
|
||||
)
|
||||
|
||||
return self._thread_pool_executor
|
||||
|
||||
|
||||
class LanguageServer(Server):
|
||||
"""The default LanguageServer
|
||||
|
||||
This class can be extended and it can be passed as a first argument to
|
||||
registered commands/features.
|
||||
|
||||
.. |ServerInfo| replace:: :class:`~lsprotocol.types.InitializeResultServerInfoType`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name
|
||||
Name of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
version
|
||||
Version of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
protocol_cls
|
||||
The :class:`~pygls.protocol.LanguageServerProtocol` class definition, or any
|
||||
subclass of it.
|
||||
|
||||
max_workers
|
||||
Maximum number of workers for ``ThreadPool`` and ``ThreadPoolExecutor``
|
||||
|
||||
text_document_sync_kind
|
||||
Text document synchronization method
|
||||
|
||||
None
|
||||
No synchronization
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Full`
|
||||
Send entire document text with each update
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`
|
||||
Send only the region of text that changed with each update
|
||||
|
||||
notebook_document_sync
|
||||
Advertise :lsp:`NotebookDocument` support to the client.
|
||||
"""
|
||||
|
||||
lsp: LanguageServerProtocol
|
||||
|
||||
default_error_message = (
|
||||
"Unexpected error in LSP server, see server's logs for details"
|
||||
)
|
||||
"""
|
||||
The default error message sent to the user's editor when this server encounters an uncaught
|
||||
exception.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str,
|
||||
loop=None,
|
||||
protocol_cls: Type[LanguageServerProtocol] = LanguageServerProtocol,
|
||||
converter_factory=default_converter,
|
||||
text_document_sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
notebook_document_sync: Optional[NotebookDocumentSyncOptions] = None,
|
||||
max_workers: int = 2,
|
||||
):
|
||||
if not issubclass(protocol_cls, LanguageServerProtocol):
|
||||
raise TypeError(
|
||||
"Protocol class should be subclass of LanguageServerProtocol"
|
||||
)
|
||||
|
||||
self.name = name
|
||||
self.version = version
|
||||
self._text_document_sync_kind = text_document_sync_kind
|
||||
self._notebook_document_sync = notebook_document_sync
|
||||
self.process_id: Optional[Union[int, None]] = None
|
||||
super().__init__(protocol_cls, converter_factory, loop, max_workers)
|
||||
|
||||
def apply_edit(
|
||||
self, edit: WorkspaceEdit, label: Optional[str] = None
|
||||
) -> WorkspaceApplyEditResponse:
|
||||
"""Sends apply edit request to the client."""
|
||||
return self.lsp.apply_edit(edit, 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.lsp.apply_edit_async(edit, label)
|
||||
try:
|
||||
asyncio.run(ws_server(host, port))
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Server was cancelled")
|
||||
|
||||
def command(self, command_name: str) -> Callable[[F], F]:
|
||||
"""Decorator used to register custom commands.
|
||||
@@ -446,17 +251,16 @@ class LanguageServer(Server):
|
||||
def my_cmd(ls, a, b, c):
|
||||
pass
|
||||
"""
|
||||
return self.lsp.fm.command(command_name)
|
||||
return self.protocol.fm.command(command_name)
|
||||
|
||||
@property
|
||||
def client_capabilities(self) -> ClientCapabilities:
|
||||
"""The client's capabilities."""
|
||||
return self.lsp.client_capabilities
|
||||
def thread(self) -> Callable[[F], F]:
|
||||
"""Decorator that mark function to execute it in a thread."""
|
||||
return self.protocol.fm.thread()
|
||||
|
||||
def feature(
|
||||
self,
|
||||
feature_name: str,
|
||||
options: Optional[Any] = None,
|
||||
options: Any | None = None,
|
||||
) -> Callable[[F], F]:
|
||||
"""Decorator used to register LSP features.
|
||||
|
||||
@@ -468,149 +272,12 @@ class LanguageServer(Server):
|
||||
def completions(ls, params: CompletionParams):
|
||||
return CompletionList(is_incomplete=False, items=[CompletionItem("Completion 1")])
|
||||
"""
|
||||
return self.lsp.fm.feature(feature_name, options)
|
||||
|
||||
def get_configuration(
|
||||
self,
|
||||
params: WorkspaceConfigurationParams,
|
||||
callback: Optional[ConfigCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Gets the configuration settings from the client."""
|
||||
return self.lsp.get_configuration(params, callback)
|
||||
|
||||
def get_configuration_async(
|
||||
self, params: WorkspaceConfigurationParams
|
||||
) -> asyncio.Future:
|
||||
"""Gets the configuration settings from the client. Should be called with `await`"""
|
||||
return self.lsp.get_configuration_async(params)
|
||||
|
||||
def log_trace(self, message: str, verbose: Optional[str] = None) -> None:
|
||||
"""Sends trace notification to the client."""
|
||||
self.lsp.log_trace(message, verbose)
|
||||
return self.protocol.fm.feature(feature_name, options)
|
||||
|
||||
@property
|
||||
def progress(self) -> Progress:
|
||||
"""Gets the object to manage client's progress bar."""
|
||||
return self.lsp.progress
|
||||
def thread_pool(self) -> ThreadPoolExecutor:
|
||||
"""Returns thread pool instance (lazy initialization)."""
|
||||
if not self._thread_pool:
|
||||
self._thread_pool = ThreadPoolExecutor(max_workers=self._max_workers)
|
||||
|
||||
def publish_diagnostics(
|
||||
self,
|
||||
uri: str,
|
||||
diagnostics: Optional[List[Diagnostic]] = None,
|
||||
version: Optional[int] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Sends diagnostic notification to the client.
|
||||
"""
|
||||
params = self.lsp._construct_publish_diagnostic_type(
|
||||
uri, diagnostics, version, **kwargs
|
||||
)
|
||||
self.lsp.publish_diagnostics(params, **kwargs)
|
||||
|
||||
def register_capability(
|
||||
self, params: RegistrationParams, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Register a new capability on the client."""
|
||||
return self.lsp.register_capability(params, callback)
|
||||
|
||||
def register_capability_async(self, params: RegistrationParams) -> asyncio.Future:
|
||||
"""Register a new capability on the client. Should be called with `await`"""
|
||||
return self.lsp.register_capability_async(params)
|
||||
|
||||
def semantic_tokens_refresh(
|
||||
self, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Request a refresh of all semantic tokens."""
|
||||
return self.lsp.semantic_tokens_refresh(callback)
|
||||
|
||||
def semantic_tokens_refresh_async(self) -> asyncio.Future:
|
||||
"""Request a refresh of all semantic tokens. Should be called with `await`"""
|
||||
return self.lsp.semantic_tokens_refresh_async()
|
||||
|
||||
def send_notification(self, method: str, params: object = None) -> None:
|
||||
"""Sends notification to the client."""
|
||||
self.lsp.notify(method, params)
|
||||
|
||||
@property
|
||||
def server_capabilities(self) -> ServerCapabilities:
|
||||
"""Return server capabilities."""
|
||||
return self.lsp.server_capabilities
|
||||
|
||||
def show_document(
|
||||
self,
|
||||
params: ShowDocumentParams,
|
||||
callback: Optional[ShowDocumentCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Display a particular document in the user interface."""
|
||||
return self.lsp.show_document(params, callback)
|
||||
|
||||
def show_document_async(self, params: ShowDocumentParams) -> asyncio.Future:
|
||||
"""Display a particular document in the user interface. Should be called with `await`"""
|
||||
return self.lsp.show_document_async(params)
|
||||
|
||||
def show_message(self, message, msg_type=MessageType.Info) -> None:
|
||||
"""Sends message to the client to display message."""
|
||||
self.lsp.show_message(message, msg_type)
|
||||
|
||||
def show_message_log(self, message, msg_type=MessageType.Log) -> None:
|
||||
"""Sends message to the client's output channel."""
|
||||
self.lsp.show_message_log(message, msg_type)
|
||||
|
||||
def _report_server_error(
|
||||
self,
|
||||
error: Exception,
|
||||
source: ServerErrors,
|
||||
):
|
||||
# Prevent recursive error reporting
|
||||
try:
|
||||
self.report_server_error(error, source)
|
||||
except Exception:
|
||||
logger.warning("Failed to report error to client")
|
||||
|
||||
def report_server_error(self, error: Exception, source: ServerErrors):
|
||||
"""
|
||||
Sends error to the client for displaying.
|
||||
|
||||
By default this fucntion does not handle LSP request errors. This is because LSP requests
|
||||
require direct responses and so already have a mechanism for including unexpected errors
|
||||
in the response body.
|
||||
|
||||
All other errors are "out of band" in the sense that the client isn't explicitly waiting
|
||||
for them. For example diagnostics are returned as notifications, not responses to requests,
|
||||
and so can seemingly be sent at random. Also for example consider JSON RPC serialization
|
||||
and deserialization, if a payload cannot be parsed then the whole request/response cycle
|
||||
cannot be completed and so one of these "out of band" error messages is sent.
|
||||
|
||||
These "out of band" error messages are not a requirement of the LSP spec. Pygls simply
|
||||
offers this behaviour as a recommended default. It is perfectly reasonble to override this
|
||||
default.
|
||||
"""
|
||||
|
||||
if source == FeatureRequestError:
|
||||
return
|
||||
|
||||
self.show_message(self.default_error_message, msg_type=MessageType.Error)
|
||||
|
||||
def thread(self) -> Callable[[F], F]:
|
||||
"""Decorator that mark function to execute it in a thread."""
|
||||
return self.lsp.thread()
|
||||
|
||||
def unregister_capability(
|
||||
self,
|
||||
params: UnregistrationParams,
|
||||
callback: Optional[Callable[[], None]] = None,
|
||||
) -> Future:
|
||||
"""Unregister a new capability on the client."""
|
||||
return self.lsp.unregister_capability(params, callback)
|
||||
|
||||
def unregister_capability_async(
|
||||
self, params: UnregistrationParams
|
||||
) -> asyncio.Future:
|
||||
"""Unregister a new capability on the client. Should be called with `await`"""
|
||||
return self.lsp.unregister_capability_async(params)
|
||||
|
||||
@property
|
||||
def workspace(self) -> Workspace:
|
||||
"""Returns in-memory workspace."""
|
||||
return self.lsp.workspace
|
||||
return self._thread_pool
|
||||
|
||||
@@ -21,6 +21,8 @@ A collection of URI utilities with logic built on the VSCode URI library.
|
||||
|
||||
https://github.com/Microsoft/vscode-uri/blob/e59cab84f5df6265aed18ae5f43552d3eef13bb9/lib/index.ts
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import re
|
||||
@@ -75,20 +77,22 @@ def from_fs_path(path: str):
|
||||
return None
|
||||
|
||||
|
||||
def to_fs_path(uri: str):
|
||||
def to_fs_path(uri: str) -> str | None:
|
||||
"""
|
||||
Returns the filesystem path of the given URI.
|
||||
|
||||
Will handle UNC paths and normalize windows drive letters to lower-case.
|
||||
Also uses the platform specific path separator. Will *not* validate the
|
||||
path for invalid characters and semantics.
|
||||
Will *not* look at the scheme of this URI.
|
||||
"""
|
||||
try:
|
||||
# scheme://netloc/path;parameters?query#fragment
|
||||
scheme, netloc, path, _, _, _ = urlparse(uri)
|
||||
|
||||
if netloc and path and scheme == "file":
|
||||
if scheme != "file":
|
||||
return None
|
||||
|
||||
if netloc and path:
|
||||
# unc path: file://shares/c$/far/boo
|
||||
value = f"//{netloc}{path}"
|
||||
|
||||
|
||||
@@ -1,97 +1,11 @@
|
||||
from typing import List
|
||||
import warnings
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from .workspace import Workspace
|
||||
from .text_document import TextDocument
|
||||
from .position_codec import PositionCodec
|
||||
|
||||
# For backwards compatibility
|
||||
Document = TextDocument
|
||||
|
||||
|
||||
def utf16_unit_offset(chars: str):
|
||||
warnings.warn(
|
||||
"'utf16_unit_offset' has been deprecated, instead use "
|
||||
"'PositionCodec.utf16_unit_offset' via 'workspace.position_codec' "
|
||||
"or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.utf16_unit_offset(chars)
|
||||
|
||||
|
||||
def utf16_num_units(chars: str):
|
||||
warnings.warn(
|
||||
"'utf16_num_units' has been deprecated, instead use "
|
||||
"'PositionCodec.client_num_units' via 'workspace.position_codec' "
|
||||
"or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.client_num_units(chars)
|
||||
|
||||
|
||||
def position_from_utf16(lines: List[str], position: types.Position):
|
||||
warnings.warn(
|
||||
"'position_from_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.position_from_client_units' via "
|
||||
"'workspace.position_codec' or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.position_from_client_units(lines, position)
|
||||
|
||||
|
||||
def position_to_utf16(lines: List[str], position: types.Position):
|
||||
warnings.warn(
|
||||
"'position_to_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.position_to_client_units' via "
|
||||
"'workspace.position_codec' or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.position_to_client_units(lines, position)
|
||||
|
||||
|
||||
def range_from_utf16(lines: List[str], range: types.Range):
|
||||
warnings.warn(
|
||||
"'range_from_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.range_from_client_units' via "
|
||||
"'workspace.position_codec' or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.range_from_client_units(lines, range)
|
||||
|
||||
|
||||
def range_to_utf16(lines: List[str], range: types.Range):
|
||||
warnings.warn(
|
||||
"'range_to_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.range_to_client_units' via 'workspace.position_codec' "
|
||||
"or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.range_to_client_units(lines, range)
|
||||
|
||||
from .position_codec import PositionCodec, ServerTextPosition, ServerTextRange
|
||||
|
||||
__all__ = (
|
||||
"Workspace",
|
||||
"TextDocument",
|
||||
"PositionCodec",
|
||||
"Document",
|
||||
"utf16_unit_offset",
|
||||
"utf16_num_units",
|
||||
"position_from_utf16",
|
||||
"position_to_utf16",
|
||||
"range_from_utf16",
|
||||
"range_to_utf16",
|
||||
"ServerTextPosition",
|
||||
"ServerTextRange",
|
||||
)
|
||||
|
||||
@@ -17,14 +17,113 @@
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
import logging
|
||||
from typing import List, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Union, Sequence, Any
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(order=True)
|
||||
class ServerTextPosition:
|
||||
line: int
|
||||
character: int
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.line}:{self.character}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServerTextRange:
|
||||
start: ServerTextPosition
|
||||
end: ServerTextPosition
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.start}-{self.end}"
|
||||
|
||||
def __contains__(self, position: Any) -> bool:
|
||||
if not isinstance(position, ServerTextPosition):
|
||||
raise TypeError("ServerTextRanges can only contain ServerTextPositions.")
|
||||
return self.start <= position <= self.end
|
||||
|
||||
def includes(self, inner: "ServerTextRange") -> bool:
|
||||
"""
|
||||
Returns whether `inner` is entirely contained within self, i.e. all
|
||||
positions in `inner` are also in `self`.
|
||||
"""
|
||||
return self.start <= inner.start and inner.end <= self.end
|
||||
|
||||
def overlaps(self, other: "ServerTextRange") -> bool:
|
||||
"""
|
||||
Returns whether `self` and `other` overlap, i.e. any positions exist that
|
||||
are included in both self and other.
|
||||
"""
|
||||
return self.start <= other.end and other.start <= self.end
|
||||
|
||||
|
||||
class UnitCounter:
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
"""
|
||||
Get the number of code units used to encode the given single character.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def num_units(self, chars: str) -> int:
|
||||
"""
|
||||
Get the number of code units used to encode the given string.
|
||||
"""
|
||||
return sum(self.code_units_for_char(c) for c in chars)
|
||||
|
||||
def column_from_utf32(self, line: str, column: int) -> int:
|
||||
"""
|
||||
Convert the codepoint index `column` into code units.
|
||||
"""
|
||||
return sum(self.code_units_for_char(c) for c in line[:column])
|
||||
|
||||
|
||||
class Utf32(UnitCounter):
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
return 1
|
||||
|
||||
def num_units(self, chars: str) -> int:
|
||||
# We can avoid the loop needed for other encodings here
|
||||
return len(chars)
|
||||
|
||||
def column_from_utf32(self, line: str, column: int) -> int:
|
||||
return column
|
||||
|
||||
|
||||
def is_beyond_basic_multilingual_plane(char: str) -> bool:
|
||||
return ord(char) > 0xFFFF
|
||||
|
||||
|
||||
class Utf16(UnitCounter):
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
if is_beyond_basic_multilingual_plane(char):
|
||||
return 2
|
||||
return 1
|
||||
|
||||
|
||||
class Utf8(UnitCounter):
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
codepoint = ord(char)
|
||||
if codepoint < 0x80:
|
||||
return 1
|
||||
if codepoint < 0x800:
|
||||
return 2
|
||||
if codepoint < 0x10000:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
|
||||
impls: dict["str | types.PositionEncodingKind | None", UnitCounter] = {
|
||||
types.PositionEncodingKind.Utf8: Utf8(),
|
||||
types.PositionEncodingKind.Utf16: Utf16(),
|
||||
types.PositionEncodingKind.Utf32: Utf32(),
|
||||
}
|
||||
|
||||
|
||||
class PositionCodec:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -33,39 +132,17 @@ class PositionCodec:
|
||||
] = types.PositionEncodingKind.Utf16,
|
||||
):
|
||||
self.encoding = encoding
|
||||
self.impl = impls.get(encoding, Utf16())
|
||||
|
||||
@classmethod
|
||||
def is_char_beyond_multilingual_plane(cls, char: str) -> bool:
|
||||
return ord(char) > 0xFFFF
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}, encoding {self.encoding}>"
|
||||
|
||||
def utf16_unit_offset(self, chars: str):
|
||||
"""
|
||||
Calculate the number of characters which need two utf-16 code units.
|
||||
|
||||
Arguments:
|
||||
chars (str): The string to count occurrences of utf-16 code units for.
|
||||
"""
|
||||
return sum(self.is_char_beyond_multilingual_plane(ch) for ch in chars)
|
||||
|
||||
def client_num_units(self, chars: str):
|
||||
"""
|
||||
Calculate the length of `str` in client-supported UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
chars (str): The string to return the length in UTF-[32|16|8] code units for.
|
||||
"""
|
||||
utf32_units = len(chars)
|
||||
if self.encoding == types.PositionEncodingKind.Utf32:
|
||||
return utf32_units
|
||||
|
||||
if self.encoding == types.PositionEncodingKind.Utf8:
|
||||
return utf32_units + (self.utf16_unit_offset(chars) * 2)
|
||||
|
||||
return utf32_units + self.utf16_unit_offset(chars)
|
||||
def client_num_units(self, string: str):
|
||||
return self.impl.num_units(string)
|
||||
|
||||
def position_from_client_units(
|
||||
self, lines: List[str], position: types.Position
|
||||
) -> types.Position:
|
||||
self, lines: Sequence[str], position: types.Position
|
||||
) -> ServerTextPosition:
|
||||
"""
|
||||
Convert the position.character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
@@ -84,7 +161,7 @@ class PositionCodec:
|
||||
see: https://github.com/microsoft/language-server-protocol/issues/376
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
@@ -93,59 +170,42 @@ class PositionCodec:
|
||||
The position with `character` being converted to UTF-32 code units.
|
||||
"""
|
||||
if len(lines) == 0:
|
||||
return types.Position(0, 0)
|
||||
return ServerTextPosition(0, 0)
|
||||
if position.line >= len(lines):
|
||||
return types.Position(len(lines) - 1, self.client_num_units(lines[-1]))
|
||||
return ServerTextPosition(len(lines) - 1, self.impl.num_units(lines[-1]))
|
||||
|
||||
_line = lines[position.line]
|
||||
_line = _line.replace("\r\n", "\n") # TODO: it's a bit of a hack
|
||||
_client_len = self.client_num_units(_line)
|
||||
_utf32_len = len(_line)
|
||||
_client_len = self.impl.num_units(_line)
|
||||
|
||||
if _client_len == 0:
|
||||
return types.Position(position.line, 0)
|
||||
return ServerTextPosition(position.line, 0)
|
||||
|
||||
_client_end_of_line = self.client_num_units(_line)
|
||||
if position.character > _client_end_of_line:
|
||||
position.character = _client_end_of_line - 1
|
||||
if position.character > _client_len:
|
||||
position.character = _client_len - 1
|
||||
|
||||
_client_index = 0
|
||||
client_position = 0
|
||||
utf32_index = 0
|
||||
while True:
|
||||
_is_searching_queried_position = _client_index < position.character
|
||||
_is_before_end_of_line = utf32_index < _utf32_len
|
||||
_is_searching_for_position = (
|
||||
_is_searching_queried_position and _is_before_end_of_line
|
||||
)
|
||||
if not _is_searching_for_position:
|
||||
for c in _line:
|
||||
if client_position >= position.character:
|
||||
break
|
||||
|
||||
_current_char = _line[utf32_index]
|
||||
_is_double_width = PositionCodec.is_char_beyond_multilingual_plane(
|
||||
_current_char
|
||||
)
|
||||
if _is_double_width:
|
||||
if self.encoding == types.PositionEncodingKind.Utf32:
|
||||
_client_index += 1
|
||||
if self.encoding == types.PositionEncodingKind.Utf8:
|
||||
_client_index += 4
|
||||
_client_index += 2
|
||||
else:
|
||||
_client_index += 1
|
||||
client_position += self.impl.code_units_for_char(c)
|
||||
utf32_index += 1
|
||||
|
||||
position = types.Position(line=position.line, character=utf32_index)
|
||||
return position
|
||||
if client_position < position.character:
|
||||
utf32_index = len(_line)
|
||||
|
||||
return ServerTextPosition(line=position.line, character=utf32_index)
|
||||
|
||||
def position_to_client_units(
|
||||
self, lines: List[str], position: types.Position
|
||||
self, lines: Sequence[str], position: "ServerTextPosition | types.Position"
|
||||
) -> types.Position:
|
||||
"""
|
||||
Convert the position.character from its internal UTF-32 representation
|
||||
to client-supported UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-32 code units.
|
||||
@@ -154,9 +214,7 @@ class PositionCodec:
|
||||
The position with `character` being converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
try:
|
||||
character = self.client_num_units(
|
||||
lines[position.line][: position.character]
|
||||
)
|
||||
character = self.impl.num_units(lines[position.line][: position.character])
|
||||
return types.Position(
|
||||
line=position.line,
|
||||
character=character,
|
||||
@@ -165,13 +223,13 @@ class PositionCodec:
|
||||
return types.Position(line=len(lines), character=0)
|
||||
|
||||
def range_from_client_units(
|
||||
self, lines: List[str], range: types.Range
|
||||
) -> types.Range:
|
||||
self, lines: Sequence[str], range: types.Range
|
||||
) -> ServerTextRange:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
@@ -179,26 +237,25 @@ class PositionCodec:
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-32 code units.
|
||||
"""
|
||||
range_new = types.Range(
|
||||
return ServerTextRange(
|
||||
start=self.position_from_client_units(lines, range.start),
|
||||
end=self.position_from_client_units(lines, range.end),
|
||||
)
|
||||
return range_new
|
||||
|
||||
def range_to_client_units(
|
||||
self, lines: List[str], range: types.Range
|
||||
self, lines: Sequence[str], range: "ServerTextRange | types.Range"
|
||||
) -> types.Range:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-32 to UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in code units.
|
||||
The line and character offset in code points.
|
||||
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-[32|16|8] code units.
|
||||
The range with `character` offsets converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
return types.Range(
|
||||
start=self.position_to_client_units(lines, range.start),
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
from typing import List, Optional, Pattern
|
||||
from typing import Optional, Pattern, Sequence
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.uris import to_fs_path
|
||||
from .position_codec import PositionCodec
|
||||
from pygls.uris import urlparse, to_fs_path
|
||||
from .position_codec import PositionCodec, ServerTextPosition, ServerTextRange
|
||||
|
||||
# TODO: this is not the best e.g. we capture numbers
|
||||
RE_END_WORD = re.compile("^[A-Za-z_0-9]*")
|
||||
@@ -47,9 +48,10 @@ class TextDocument(object):
|
||||
):
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
path = to_fs_path(uri)
|
||||
if path is None:
|
||||
raise Exception("`path` cannot be None")
|
||||
|
||||
if (path := to_fs_path(uri)) is None:
|
||||
_, _, path, *_ = urlparse(uri)
|
||||
|
||||
self.path = path
|
||||
self.language_id = language_id
|
||||
self.filename: Optional[str] = os.path.basename(self.path)
|
||||
@@ -73,7 +75,7 @@ class TextDocument(object):
|
||||
return self._position_codec
|
||||
|
||||
def _apply_incremental_change(
|
||||
self, change: types.TextDocumentContentChangeEvent_Type1
|
||||
self, change: types.TextDocumentContentChangePartial
|
||||
) -> None:
|
||||
"""Apply an ``Incremental`` text change to the document"""
|
||||
lines = self.lines
|
||||
@@ -142,7 +144,7 @@ class TextDocument(object):
|
||||
content update client requests in the pygls Python library.
|
||||
|
||||
"""
|
||||
if isinstance(change, types.TextDocumentContentChangeEvent_Type1):
|
||||
if isinstance(change, types.TextDocumentContentChangePartial):
|
||||
if self._is_sync_kind_incremental:
|
||||
self._apply_incremental_change(change)
|
||||
return
|
||||
@@ -161,26 +163,101 @@ class TextDocument(object):
|
||||
self._apply_full_change(change)
|
||||
|
||||
@property
|
||||
def lines(self) -> List[str]:
|
||||
return self.source.splitlines(True)
|
||||
def lines(self) -> Sequence[str]:
|
||||
return tuple(self.source.splitlines(True))
|
||||
|
||||
def offset_at_server_position(self, server_position: ServerTextPosition) -> int:
|
||||
"""
|
||||
Convert server_position to an index into self.source.
|
||||
|
||||
The index is the number of code points preceding the client_position in self.source.
|
||||
"""
|
||||
row, col = server_position.line, server_position.character
|
||||
return col + sum(len(line) for line in self.lines[:row])
|
||||
|
||||
def offset_at_position(self, client_position: types.Position) -> int:
|
||||
"""Return the character offset pointed at by the given client_position."""
|
||||
"""
|
||||
Convert client_position to an index into self.source.
|
||||
|
||||
The index is the number of code points preceding the client_position in self.source.
|
||||
|
||||
Example in a code action request handler:
|
||||
selected_string = document.source[
|
||||
document.offset_at_position(params.range.start) : document.offset_at_position(params.range.end)
|
||||
]
|
||||
"""
|
||||
lines = self.lines
|
||||
server_position = self._position_codec.position_from_client_units(
|
||||
lines, client_position
|
||||
)
|
||||
row, col = server_position.line, server_position.character
|
||||
return col + sum(
|
||||
self._position_codec.client_num_units(line) for line in lines[:row]
|
||||
)
|
||||
return self.offset_at_server_position(server_position)
|
||||
|
||||
def server_position_at_offset(self, offset: int) -> ServerTextPosition:
|
||||
"""
|
||||
Convert a numeric character offset (index into self.source) into a line-column position.
|
||||
"""
|
||||
remaining_offset = offset
|
||||
for lineno, line in enumerate(self.lines):
|
||||
if remaining_offset < len(line):
|
||||
return ServerTextPosition(lineno, remaining_offset)
|
||||
remaining_offset -= len(line)
|
||||
# The desired position is beyond the end of the last line.
|
||||
return ServerTextPosition(lineno + 1, 0)
|
||||
|
||||
def client_position_at_offset(self, offset: int) -> types.Position:
|
||||
"""
|
||||
Convert a numeric character offset (index into self.source) into a line-column position in client units.
|
||||
"""
|
||||
return self.position_to_client_units(self.server_position_at_offset(offset))
|
||||
|
||||
def range_from_client_units(self, range: types.Range) -> ServerTextRange:
|
||||
"""
|
||||
Convert a range from client units into code points, suitable for indexing into `self.lines`.
|
||||
"""
|
||||
return self.position_codec.range_from_client_units(self.lines, range)
|
||||
|
||||
def position_from_client_units(
|
||||
self, position: types.Position
|
||||
) -> ServerTextPosition:
|
||||
"""
|
||||
Convert a position from client units into code points, suitable for indexing into `self.lines`.
|
||||
"""
|
||||
return self.position_codec.position_from_client_units(self.lines, position)
|
||||
|
||||
def range_to_client_units(self, range: ServerTextRange) -> types.Range:
|
||||
"""
|
||||
Convert a range from code points into client units, suitable for sending to the client.
|
||||
"""
|
||||
return self.position_codec.range_to_client_units(self.lines, range)
|
||||
|
||||
def position_to_client_units(self, position: ServerTextPosition) -> types.Position:
|
||||
"""
|
||||
Convert a position from code points into client units, suitable for sending to the client.
|
||||
"""
|
||||
return self.position_codec.position_to_client_units(self.lines, position)
|
||||
|
||||
def text_in_client_range(self, range: types.Range) -> str:
|
||||
"""
|
||||
Given a range in client units, return the text in this range in this document.
|
||||
"""
|
||||
return self.text_in_server_range(self.range_from_client_units(range))
|
||||
|
||||
def text_in_server_range(self, range: ServerTextRange) -> str:
|
||||
"""
|
||||
Given a range in server units, return the text in this range in this document.
|
||||
"""
|
||||
return self.source[
|
||||
self.offset_at_server_position(
|
||||
range.start
|
||||
) : self.offset_at_server_position(range.end)
|
||||
]
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
if self._source is None:
|
||||
with io.open(self.path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
return self._source
|
||||
if self._source is None and self.path is not None:
|
||||
return pathlib.Path(self.path).read_text(encoding="utf-8")
|
||||
|
||||
return self._source or ""
|
||||
|
||||
def word_at_position(
|
||||
self,
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Dict, Optional, Sequence, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from lsprotocol import types
|
||||
from lsprotocol.types import (
|
||||
@@ -40,7 +40,7 @@ class Workspace(object):
|
||||
self,
|
||||
root_uri: Optional[str],
|
||||
sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
workspace_folders: Optional[List[WorkspaceFolder]] = None,
|
||||
workspace_folders: Optional[Sequence[WorkspaceFolder]] = None,
|
||||
position_encoding: Optional[
|
||||
Union[PositionEncodingKind, str]
|
||||
] = PositionEncodingKind.Utf16,
|
||||
@@ -48,10 +48,7 @@ class Workspace(object):
|
||||
self._root_uri = root_uri
|
||||
if self._root_uri is not None:
|
||||
self._root_uri_scheme = uri_scheme(self._root_uri)
|
||||
root_path = to_fs_path(self._root_uri)
|
||||
if root_path is None:
|
||||
raise Exception("Couldn't get `root_path` from `root_uri`")
|
||||
self._root_path = root_path
|
||||
self._root_path = to_fs_path(self._root_uri)
|
||||
else:
|
||||
self._root_path = None
|
||||
self._sync_kind = sync_kind
|
||||
@@ -94,17 +91,7 @@ class Workspace(object):
|
||||
)
|
||||
|
||||
def add_folder(self, folder: WorkspaceFolder):
|
||||
self._folders[folder.uri] = folder
|
||||
|
||||
@property
|
||||
def documents(self):
|
||||
warnings.warn(
|
||||
"'workspace.documents' has been deprecated, use "
|
||||
"'workspace.text_documents' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.text_documents
|
||||
self._folders[unquote(folder.uri)] = folder
|
||||
|
||||
@property
|
||||
def notebook_documents(self):
|
||||
@@ -141,10 +128,10 @@ class Workspace(object):
|
||||
The requested notebook document if found, ``None`` otherwise.
|
||||
"""
|
||||
if notebook_uri is not None:
|
||||
return self._notebook_documents.get(notebook_uri)
|
||||
return self._notebook_documents.get(unquote(notebook_uri))
|
||||
|
||||
if cell_uri is not None:
|
||||
notebook_uri = self._cell_in_notebook.get(cell_uri)
|
||||
notebook_uri = self._cell_in_notebook.get(unquote(cell_uri))
|
||||
if notebook_uri is None:
|
||||
return None
|
||||
|
||||
@@ -159,18 +146,25 @@ class Workspace(object):
|
||||
|
||||
See https://github.com/Microsoft/language-server-protocol/issues/177
|
||||
"""
|
||||
return self._text_documents.get(doc_uri) or self._create_text_document(doc_uri)
|
||||
return self._text_documents.get(unquote(doc_uri)) or self._create_text_document(
|
||||
doc_uri
|
||||
)
|
||||
|
||||
def is_local(self):
|
||||
return (
|
||||
self._root_uri_scheme == "" or self._root_uri_scheme == "file"
|
||||
) and os.path.exists(self._root_path)
|
||||
|
||||
if self._root_uri_scheme not in {"", "file"}:
|
||||
return False
|
||||
|
||||
if (path := self._root_path) is None:
|
||||
return False
|
||||
|
||||
return os.path.exists(path)
|
||||
|
||||
def put_notebook_document(self, params: types.DidOpenNotebookDocumentParams):
|
||||
notebook = params.notebook_document
|
||||
|
||||
# Create a fresh instance to ensure our copy cannot be accidentally modified.
|
||||
self._notebook_documents[notebook.uri] = copy.deepcopy(notebook)
|
||||
self._notebook_documents[unquote(notebook.uri)] = copy.deepcopy(notebook)
|
||||
|
||||
for cell_document in params.cell_text_documents:
|
||||
self.put_text_document(cell_document, notebook_uri=notebook.uri)
|
||||
@@ -193,7 +187,7 @@ class Workspace(object):
|
||||
"""
|
||||
doc_uri = text_document.uri
|
||||
|
||||
self._text_documents[doc_uri] = self._create_text_document(
|
||||
self._text_documents[unquote(doc_uri)] = self._create_text_document(
|
||||
doc_uri,
|
||||
source=text_document.text,
|
||||
version=text_document.version,
|
||||
@@ -201,23 +195,23 @@ class Workspace(object):
|
||||
)
|
||||
|
||||
if notebook_uri:
|
||||
self._cell_in_notebook[doc_uri] = notebook_uri
|
||||
self._cell_in_notebook[unquote(doc_uri)] = unquote(notebook_uri)
|
||||
|
||||
def remove_notebook_document(self, params: types.DidCloseNotebookDocumentParams):
|
||||
notebook_uri = params.notebook_document.uri
|
||||
self._notebook_documents.pop(notebook_uri, None)
|
||||
self._notebook_documents.pop(unquote(notebook_uri), None)
|
||||
|
||||
for cell_document in params.cell_text_documents:
|
||||
self.remove_text_document(cell_document.uri)
|
||||
|
||||
def remove_text_document(self, doc_uri: str):
|
||||
self._text_documents.pop(doc_uri, None)
|
||||
self._cell_in_notebook.pop(doc_uri, None)
|
||||
self._text_documents.pop(unquote(doc_uri), None)
|
||||
self._cell_in_notebook.pop(unquote(doc_uri), None)
|
||||
|
||||
def remove_folder(self, folder_uri: str):
|
||||
self._folders.pop(folder_uri, None)
|
||||
self._folders.pop(unquote(folder_uri), None)
|
||||
try:
|
||||
del self._folders[folder_uri]
|
||||
del self._folders[unquote(folder_uri)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -231,7 +225,7 @@ class Workspace(object):
|
||||
|
||||
def update_notebook_document(self, params: types.DidChangeNotebookDocumentParams):
|
||||
uri = params.notebook_document.uri
|
||||
notebook = self._notebook_documents[uri]
|
||||
notebook = self._notebook_documents[unquote(uri)]
|
||||
notebook.version = params.notebook_document.version
|
||||
|
||||
if params.change.metadata:
|
||||
@@ -283,41 +277,5 @@ class Workspace(object):
|
||||
change: types.TextDocumentContentChangeEvent,
|
||||
):
|
||||
doc_uri = text_doc.uri
|
||||
self._text_documents[doc_uri].apply_change(change)
|
||||
self._text_documents[doc_uri].version = text_doc.version
|
||||
|
||||
def get_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.get_document' has been deprecated, use "
|
||||
"'workspace.get_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.get_text_document(*args, **kwargs)
|
||||
|
||||
def remove_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.remove_document' has been deprecated, use "
|
||||
"'workspace.remove_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.remove_text_document(*args, **kwargs)
|
||||
|
||||
def put_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.put_document' has been deprecated, use "
|
||||
"'workspace.put_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.put_text_document(*args, **kwargs)
|
||||
|
||||
def update_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.update_document' has been deprecated, use "
|
||||
"'workspace.update_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.update_text_document(*args, **kwargs)
|
||||
self._text_documents[unquote(doc_uri)].apply_change(change)
|
||||
self._text_documents[unquote(doc_uri)].version = text_doc.version
|
||||
|
||||
Reference in New Issue
Block a user