Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UTF-8 and UTF-32 position encoding support #375

Merged
merged 8 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 33 additions & 2 deletions pygls/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
# limitations under the License. #
############################################################################
from functools import reduce
from typing import Any, Dict, List, Set, Union
from typing import Any, Dict, List, Optional, Set, Union
import logging

from lsprotocol.types import (
INLAY_HINT_RESOLVE,
Expand Down Expand Up @@ -64,6 +65,7 @@
WORKSPACE_WILL_DELETE_FILES,
WORKSPACE_WILL_RENAME_FILES,
InlayHintOptions,
PositionEncodingKind,
)
from lsprotocol.types import (
ClientCapabilities,
Expand All @@ -86,6 +88,8 @@
WorkspaceFoldersServerCapabilities,
)

logger = logging.getLogger(__name__)


def get_capability(
client_capabilities: ClientCapabilities, field: str, default: Any = None
Expand Down Expand Up @@ -115,7 +119,7 @@ def __init__(
feature_options: Dict[str, Any],
commands: List[str],
text_document_sync_kind: TextDocumentSyncKind,
notebook_document_sync: NotebookDocumentSyncOptions,
notebook_document_sync: Optional[NotebookDocumentSyncOptions] = None,
):
self.client_capabilities = client_capabilities
self.features = features
Expand Down Expand Up @@ -429,6 +433,32 @@ def _with_inline_value_provider(self):
self.server_cap.inline_value_provider = value
return self

def _with_position_encodings(self):
self.server_cap.position_encoding = PositionEncodingKind.Utf16

general = self.client_capabilities.general
if general is None:
return self

encodings = general.position_encodings
if encodings is None:
return self

if PositionEncodingKind.Utf16 in encodings:
return self

if PositionEncodingKind.Utf32 in encodings:
self.server_cap.position_encoding = PositionEncodingKind.Utf32
return self

if PositionEncodingKind.Utf8 in encodings:
self.server_cap.position_encoding = PositionEncodingKind.Utf8
return self

logger.warning(f"Unknown `PositionEncoding`s: {encodings}")

return self

def _build(self):
return self.server_cap

Expand Down Expand Up @@ -467,5 +497,6 @@ def build(self):
._with_workspace_capabilities()
._with_diagnostic_provider()
._with_inline_value_provider()
._with_position_encodings()
._build()
)
5 changes: 4 additions & 1 deletion pygls/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ def __init__(
protocol_cls: Type[JsonRPCProtocol] = JsonRPCProtocol,
converter_factory: Callable[[], Converter] = default_converter,
):
self.protocol = protocol_cls(self, converter_factory())
# 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.
self.protocol = protocol_cls(self, converter_factory()) # type: ignore

self._server: Optional[asyncio.subprocess.Process] = None
self._stop_event = Event()
Expand Down
47 changes: 37 additions & 10 deletions pygls/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and #
# limitations under the License. #
############################################################################
from __future__ import annotations
import asyncio
import enum
import functools
Expand All @@ -27,7 +28,21 @@
from concurrent.futures import Future
from functools import lru_cache, partial
from itertools import zip_longest
from typing import Any, Callable, List, Optional, Type, TypeVar, Union
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Type,
TypeVar,
Union,
TYPE_CHECKING,
)

if TYPE_CHECKING:
from pygls.server import LanguageServer, WebSocketTransportAdapter


import attrs
from cattrs.errors import ClassValidationError
Expand Down Expand Up @@ -239,19 +254,21 @@ class JsonRPCProtocol(asyncio.Protocol):

VERSION = "2.0"

def __init__(self, server, converter):
def __init__(self, server: LanguageServer, converter):
self._server = server
self._converter = converter

self._shutdown = False

# Book keeping for in-flight requests
self._request_futures = {}
self._result_types = {}
self._request_futures: Dict[str, Future[Any]] = {}
self._result_types: Dict[str, Any] = {}

self.fm = FeatureManager(server)
self.transport = None
self._message_buf = []
self.transport: Optional[
Union[asyncio.WriteTransport, WebSocketTransportAdapter]
] = None
self._message_buf: List[bytes] = []

self._send_only_body = False

Expand Down Expand Up @@ -504,7 +521,9 @@ def _send_data(self, data):
logger.info("Sending data: %s", body)

if self._send_only_body:
self.transport.write(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

header = (
Expand Down Expand Up @@ -544,7 +563,10 @@ def connection_lost(self, exc):
logger.error("Connection to the client is lost! Shutting down the server.")
sys.exit(1)

def connection_made(self, transport: asyncio.BaseTransport):
def connection_made( # type: ignore # see: https://github.com/python/typeshed/issues/3021
self,
transport: asyncio.Transport,
):
"""Method from base class, called when connection is established"""
self.transport = transport

Expand Down Expand Up @@ -805,12 +827,17 @@ def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
)

root_path = params.root_path
root_uri = params.root_uri or from_fs_path(root_path)
root_uri = params.root_uri
if root_path is not None and root_uri is None:
root_uri = from_fs_path(root_path)

# Initialize the workspace
workspace_folders = params.workspace_folders or []
self._workspace = Workspace(
root_uri, text_document_sync_kind, workspace_folders
root_uri,
text_document_sync_kind,
workspace_folders,
self.server_capabilities.position_encoding,
)

self.trace = TraceValues.Off
Expand Down
29 changes: 23 additions & 6 deletions pygls/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@
import cattrs
from pygls import IS_PYODIDE
from pygls.lsp import ConfigCallbackType, ShowDocumentCallbackType
from pygls.exceptions import PyglsError, JsonRpcException, FeatureRequestError
from pygls.exceptions import (
FeatureNotificationError,
JsonRpcInternalError,
PyglsError,
JsonRpcException,
FeatureRequestError,
)
from lsprotocol.types import (
ClientCapabilities,
Diagnostic,
Expand All @@ -62,6 +68,14 @@

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)."""
Expand Down Expand Up @@ -204,7 +218,9 @@ def __init__(
self._owns_loop = False

self.loop = loop
self.lsp = protocol_cls(self, converter_factory())

# TODO: Will move this to `LanguageServer` soon
self.lsp = protocol_cls(self, converter_factory()) # type: ignore

def shutdown(self):
"""Shutdown server."""
Expand Down Expand Up @@ -404,6 +420,7 @@ def __init__(
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(
Expand Down Expand Up @@ -541,17 +558,17 @@ def show_message_log(self, message, msg_type=MessageType.Log) -> None:
self.lsp.show_message_log(message, msg_type)

def _report_server_error(
self, error: Exception, source: Union[PyglsError, JsonRpcException]
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: Union[PyglsError, JsonRpcException]
):
def report_server_error(self, error: Exception, source: ServerErrors):
"""
Sends error to the client for displaying.

Expand Down
Loading
Loading