python lsp
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2018 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
import os
|
||||
import sys
|
||||
|
||||
IS_WIN = os.name == "nt"
|
||||
IS_PYODIDE = "pyodide" in sys.modules
|
||||
|
||||
pygls = "pygls"
|
||||
@@ -0,0 +1,460 @@
|
||||
############################################################################
|
||||
# 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 functools import reduce
|
||||
from typing import Any, Dict, List, Optional, Set, Union, TypeVar
|
||||
import logging
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class ServerCapabilitiesBuilder:
|
||||
"""Create `ServerCapabilities` instance depending on builtin and user registered
|
||||
features.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client_capabilities: types.ClientCapabilities,
|
||||
features: Set[str],
|
||||
feature_options: Dict[str, Any],
|
||||
commands: List[str],
|
||||
text_document_sync_kind: types.TextDocumentSyncKind,
|
||||
notebook_document_sync: Optional[types.NotebookDocumentSyncOptions] = None,
|
||||
):
|
||||
self.client_capabilities = client_capabilities
|
||||
self.features = features
|
||||
self.feature_options = feature_options
|
||||
self.commands = commands
|
||||
self.text_document_sync_kind = text_document_sync_kind
|
||||
self.notebook_document_sync = notebook_document_sync
|
||||
|
||||
self.server_cap = types.ServerCapabilities()
|
||||
|
||||
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
|
||||
|
||||
def _with_text_document_sync(self):
|
||||
open_close = (
|
||||
types.TEXT_DOCUMENT_DID_OPEN in self.features
|
||||
or types.TEXT_DOCUMENT_DID_CLOSE in self.features
|
||||
)
|
||||
will_save = (
|
||||
get_capability(
|
||||
self.client_capabilities, "text_document.synchronization.will_save"
|
||||
)
|
||||
and types.TEXT_DOCUMENT_WILL_SAVE in self.features
|
||||
)
|
||||
will_save_wait_until = (
|
||||
get_capability(
|
||||
self.client_capabilities,
|
||||
"text_document.synchronization.will_save_wait_until",
|
||||
)
|
||||
and types.TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL in self.features
|
||||
)
|
||||
if types.TEXT_DOCUMENT_DID_SAVE in self.features:
|
||||
save = self.feature_options.get(types.TEXT_DOCUMENT_DID_SAVE, True)
|
||||
else:
|
||||
save = False
|
||||
|
||||
self.server_cap.text_document_sync = types.TextDocumentSyncOptions(
|
||||
open_close=open_close,
|
||||
change=self.text_document_sync_kind,
|
||||
will_save=will_save,
|
||||
will_save_wait_until=will_save_wait_until,
|
||||
save=save,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def _with_notebook_document_sync(self):
|
||||
if self.client_capabilities.notebook_document is None:
|
||||
return self
|
||||
|
||||
self.server_cap.notebook_document_sync = self.notebook_document_sync
|
||||
return self
|
||||
|
||||
def _with_completion(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_COMPLETION, default=types.CompletionOptions()
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.completion_provider = value
|
||||
return self
|
||||
|
||||
def _with_hover(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_HOVER, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.hover_provider = value
|
||||
return self
|
||||
|
||||
def _with_signature_help(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_SIGNATURE_HELP, default=types.SignatureHelpOptions()
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.signature_help_provider = value
|
||||
return self
|
||||
|
||||
def _with_declaration(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_DECLARATION, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.declaration_provider = value
|
||||
return self
|
||||
|
||||
def _with_definition(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_DEFINITION, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.definition_provider = value
|
||||
return self
|
||||
|
||||
def _with_type_definition(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_TYPE_DEFINITION, default=types.TypeDefinitionOptions()
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.type_definition_provider = value
|
||||
return self
|
||||
|
||||
def _with_inlay_hints(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_INLAY_HINT, default=types.InlayHintOptions()
|
||||
)
|
||||
if value is not None:
|
||||
value.resolve_provider = types.INLAY_HINT_RESOLVE in self.features
|
||||
self.server_cap.inlay_hint_provider = value
|
||||
return self
|
||||
|
||||
def _with_implementation(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_IMPLEMENTATION, default=types.ImplementationOptions()
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.implementation_provider = value
|
||||
return self
|
||||
|
||||
def _with_references(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_REFERENCES, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.references_provider = value
|
||||
return self
|
||||
|
||||
def _with_document_highlight(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.document_highlight_provider = value
|
||||
return self
|
||||
|
||||
def _with_document_symbol(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_DOCUMENT_SYMBOL, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.document_symbol_provider = value
|
||||
return self
|
||||
|
||||
def _with_code_action(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_CODE_ACTION, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.code_action_provider = value
|
||||
return self
|
||||
|
||||
def _with_code_lens(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_CODE_LENS, default=types.CodeLensOptions()
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.code_lens_provider = value
|
||||
return self
|
||||
|
||||
def _with_document_link(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_DOCUMENT_LINK, default=types.DocumentLinkOptions()
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.document_link_provider = value
|
||||
return self
|
||||
|
||||
def _with_color(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_DOCUMENT_COLOR, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.color_provider = value
|
||||
return self
|
||||
|
||||
def _with_document_formatting(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_FORMATTING, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.document_formatting_provider = value
|
||||
return self
|
||||
|
||||
def _with_document_range_formatting(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_RANGE_FORMATTING, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.document_range_formatting_provider = value
|
||||
return self
|
||||
|
||||
def _with_document_on_type_formatting(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_ON_TYPE_FORMATTING, default=None
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.document_on_type_formatting_provider = value
|
||||
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
|
||||
return self
|
||||
|
||||
def _with_folding_range(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_FOLDING_RANGE, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.folding_range_provider = value
|
||||
return self
|
||||
|
||||
def _with_execute_command(self):
|
||||
self.server_cap.execute_command_provider = types.ExecuteCommandOptions(
|
||||
commands=self.commands
|
||||
)
|
||||
return self
|
||||
|
||||
def _with_selection_range(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_SELECTION_RANGE, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.selection_range_provider = value
|
||||
return self
|
||||
|
||||
def _with_call_hierarchy(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.call_hierarchy_provider = value
|
||||
return self
|
||||
|
||||
def _with_type_hierarchy(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.type_hierarchy_provider = value
|
||||
return self
|
||||
|
||||
def _with_semantic_tokens(self):
|
||||
providers = [
|
||||
types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
|
||||
types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA,
|
||||
types.TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE,
|
||||
]
|
||||
|
||||
value = None
|
||||
for provider in providers:
|
||||
value = self._provider_options(provider, default=None)
|
||||
if value is not None:
|
||||
break
|
||||
|
||||
if value is None:
|
||||
return self
|
||||
|
||||
if isinstance(value, types.SemanticTokensRegistrationOptions):
|
||||
self.server_cap.semantic_tokens_provider = value
|
||||
return self
|
||||
|
||||
full_support: Union[bool, types.SemanticTokensOptionsFullType1] = (
|
||||
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)
|
||||
|
||||
options = types.SemanticTokensOptions(
|
||||
legend=value,
|
||||
full=full_support or None,
|
||||
range=types.TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE in self.features or None,
|
||||
)
|
||||
|
||||
if options.full or options.range:
|
||||
self.server_cap.semantic_tokens_provider = options
|
||||
|
||||
return self
|
||||
|
||||
def _with_linked_editing_range(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_LINKED_EDITING_RANGE, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.linked_editing_range_provider = value
|
||||
return self
|
||||
|
||||
def _with_moniker(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_MONIKER, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.moniker_provider = value
|
||||
return self
|
||||
|
||||
def _with_workspace_symbol(self):
|
||||
value = self._provider_options(
|
||||
types.WORKSPACE_SYMBOL, default=types.WorkspaceSymbolOptions()
|
||||
)
|
||||
if value is not None:
|
||||
value.resolve_provider = types.WORKSPACE_SYMBOL_RESOLVE in self.features
|
||||
self.server_cap.workspace_symbol_provider = value
|
||||
return self
|
||||
|
||||
def _with_workspace_capabilities(self):
|
||||
# File operations
|
||||
file_operations = types.FileOperationOptions()
|
||||
operations = [
|
||||
(types.WORKSPACE_WILL_CREATE_FILES, "will_create"),
|
||||
(types.WORKSPACE_DID_CREATE_FILES, "did_create"),
|
||||
(types.WORKSPACE_WILL_DELETE_FILES, "will_delete"),
|
||||
(types.WORKSPACE_DID_DELETE_FILES, "did_delete"),
|
||||
(types.WORKSPACE_WILL_RENAME_FILES, "will_rename"),
|
||||
(types.WORKSPACE_DID_RENAME_FILES, "did_rename"),
|
||||
]
|
||||
|
||||
for method_name, capability_name in operations:
|
||||
client_supports_method = get_capability(
|
||||
self.client_capabilities, f"workspace.file_operations.{capability_name}"
|
||||
)
|
||||
|
||||
if client_supports_method:
|
||||
value = self._provider_options(method_name, default=None)
|
||||
setattr(file_operations, capability_name, value)
|
||||
|
||||
self.server_cap.workspace = types.ServerCapabilitiesWorkspaceType(
|
||||
workspace_folders=types.WorkspaceFoldersServerCapabilities(
|
||||
supported=True,
|
||||
change_notifications=True,
|
||||
),
|
||||
file_operations=file_operations,
|
||||
)
|
||||
return self
|
||||
|
||||
def _with_diagnostic_provider(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_DIAGNOSTIC,
|
||||
default=types.DiagnosticOptions(
|
||||
inter_file_dependencies=False, workspace_diagnostics=False
|
||||
),
|
||||
)
|
||||
if value is not None:
|
||||
value.workspace_diagnostics = types.WORKSPACE_DIAGNOSTIC in self.features
|
||||
self.server_cap.diagnostic_provider = value
|
||||
return self
|
||||
|
||||
def _with_inline_value_provider(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_INLINE_VALUE, default=True)
|
||||
if value is not None:
|
||||
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}")
|
||||
|
||||
return self
|
||||
|
||||
def _build(self):
|
||||
return self.server_cap
|
||||
|
||||
def build(self):
|
||||
return (
|
||||
self._with_text_document_sync()
|
||||
._with_notebook_document_sync()
|
||||
._with_completion()
|
||||
._with_hover()
|
||||
._with_signature_help()
|
||||
._with_declaration()
|
||||
._with_definition()
|
||||
._with_type_definition()
|
||||
._with_inlay_hints()
|
||||
._with_implementation()
|
||||
._with_references()
|
||||
._with_document_highlight()
|
||||
._with_document_symbol()
|
||||
._with_code_action()
|
||||
._with_code_lens()
|
||||
._with_document_link()
|
||||
._with_color()
|
||||
._with_document_formatting()
|
||||
._with_document_range_formatting()
|
||||
._with_document_on_type_formatting()
|
||||
._with_rename()
|
||||
._with_folding_range()
|
||||
._with_execute_command()
|
||||
._with_selection_range()
|
||||
._with_call_hierarchy()
|
||||
._with_type_hierarchy()
|
||||
._with_semantic_tokens()
|
||||
._with_linked_editing_range()
|
||||
._with_moniker()
|
||||
._with_workspace_symbol()
|
||||
._with_workspace_capabilities()
|
||||
._with_diagnostic_provider()
|
||||
._with_inline_value_provider()
|
||||
._with_position_encodings()
|
||||
._build()
|
||||
)
|
||||
@@ -0,0 +1,176 @@
|
||||
############################################################################
|
||||
# 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. #
|
||||
############################################################################
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
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.protocol import JsonRPCProtocol, default_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."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
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.
|
||||
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] = []
|
||||
|
||||
@property
|
||||
def stopped(self) -> bool:
|
||||
"""Return ``True`` if the client has been stopped."""
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def feature(
|
||||
self,
|
||||
feature_name: str,
|
||||
options: Optional[Any] = None,
|
||||
):
|
||||
"""Decorator used to register LSP features.
|
||||
|
||||
Example
|
||||
-------
|
||||
::
|
||||
|
||||
import logging
|
||||
from pygls.client import JsonRPCClient
|
||||
|
||||
ls = JsonRPCClient()
|
||||
|
||||
@ls.feature('window/logMessage')
|
||||
def completions(ls, params):
|
||||
logging.info("%s", params.message)
|
||||
"""
|
||||
return self.protocol.fm.feature(feature_name, options)
|
||||
|
||||
async def start_io(self, cmd: str, *args, **kwargs):
|
||||
"""Start the given server and communicate with it over stdio."""
|
||||
|
||||
logger.debug("Starting server process: %s", " ".join([cmd, *args]))
|
||||
server = await asyncio.create_subprocess_exec(
|
||||
cmd,
|
||||
*args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.protocol.connection_made(server.stdin) # type: ignore
|
||||
connection = asyncio.create_task(
|
||||
aio_readline(self._stop_event, server.stdout, self.protocol.data_received)
|
||||
)
|
||||
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,
|
||||
)
|
||||
await self.server_exit(self._server)
|
||||
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]
|
||||
):
|
||||
try:
|
||||
self.report_server_error(error, source)
|
||||
except Exception:
|
||||
logger.error("Unable to report error", exc_info=True)
|
||||
|
||||
def report_server_error(
|
||||
self, error: Exception, source: Union[PyglsError, JsonRpcException]
|
||||
):
|
||||
"""Called when the server does something unexpected e.g. respond with malformed
|
||||
JSON."""
|
||||
|
||||
async def stop(self):
|
||||
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()
|
||||
|
||||
if len(self._async_tasks) > 0:
|
||||
await asyncio.gather(*self._async_tasks)
|
||||
@@ -0,0 +1,26 @@
|
||||
############################################################################
|
||||
# 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. #
|
||||
############################################################################
|
||||
|
||||
# Dynamically assigned attributes
|
||||
ATTR_EXECUTE_IN_THREAD = "execute_in_thread"
|
||||
ATTR_COMMAND_TYPE = "command"
|
||||
ATTR_FEATURE_TYPE = "feature"
|
||||
ATTR_REGISTERED_NAME = "reg_name"
|
||||
ATTR_REGISTERED_TYPE = "reg_type"
|
||||
|
||||
# Parameters
|
||||
PARAM_LS = "ls"
|
||||
@@ -0,0 +1,215 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2018 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
import traceback
|
||||
from typing import Set
|
||||
from typing import Type
|
||||
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")
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.code = code or getattr(self.__class__, "CODE")
|
||||
self.data = data
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, self.__class__)
|
||||
and self.code == other.code
|
||||
and self.message == other.message
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.code, self.message))
|
||||
|
||||
@staticmethod
|
||||
def from_error(error):
|
||||
for exc_class in _EXCEPTIONS:
|
||||
if exc_class.supports_code(error.code):
|
||||
return exc_class(
|
||||
code=error.code, message=error.message, data=error.data
|
||||
)
|
||||
|
||||
return JsonRpcException(code=error.code, message=error.message, data=error.data)
|
||||
|
||||
@classmethod
|
||||
def supports_code(cls, code):
|
||||
# Defaults to UnknownErrorCode
|
||||
return getattr(cls, "CODE", -32001) == code
|
||||
|
||||
def to_response_error(self) -> ResponseError:
|
||||
return ResponseError(code=self.code, message=self.message, data=self.data)
|
||||
|
||||
|
||||
class JsonRpcInternalError(JsonRpcException):
|
||||
CODE = -32603
|
||||
MESSAGE = "Internal Error"
|
||||
|
||||
@classmethod
|
||||
def of(cls, exc_info):
|
||||
exc_type, exc_value, exc_tb = exc_info
|
||||
return cls(
|
||||
message="".join(
|
||||
traceback.format_exception_only(exc_type, exc_value)
|
||||
).strip(),
|
||||
data={"traceback": traceback.format_tb(exc_tb)},
|
||||
)
|
||||
|
||||
|
||||
class JsonRpcInvalidParams(JsonRpcException):
|
||||
CODE = -32602
|
||||
MESSAGE = "Invalid Params"
|
||||
|
||||
|
||||
class JsonRpcInvalidRequest(JsonRpcException):
|
||||
CODE = -32600
|
||||
MESSAGE = "Invalid Request"
|
||||
|
||||
|
||||
class JsonRpcMethodNotFound(JsonRpcException):
|
||||
CODE = -32601
|
||||
MESSAGE = "Method Not Found"
|
||||
|
||||
@classmethod
|
||||
def of(cls, method):
|
||||
return cls(message=cls.MESSAGE + ": " + method)
|
||||
|
||||
|
||||
class JsonRpcParseError(JsonRpcException):
|
||||
CODE = -32700
|
||||
MESSAGE = "Parse Error"
|
||||
|
||||
|
||||
class JsonRpcRequestCancelled(JsonRpcException):
|
||||
CODE = -32800
|
||||
MESSAGE = "Request Cancelled"
|
||||
|
||||
|
||||
class JsonRpcContentModified(JsonRpcException):
|
||||
CODE = -32801
|
||||
MESSAGE = "Content Modified"
|
||||
|
||||
|
||||
class JsonRpcServerNotInitialized(JsonRpcException):
|
||||
CODE = -32002
|
||||
MESSAGE = "ServerNotInitialized"
|
||||
|
||||
|
||||
class JsonRpcUnknownErrorCode(JsonRpcException):
|
||||
CODE = -32001
|
||||
MESSAGE = "UnknownErrorCode"
|
||||
|
||||
|
||||
class JsonRpcReservedErrorRangeStart(JsonRpcException):
|
||||
CODE = -32099
|
||||
MESSAGE = "jsonrpcReservedErrorRangeStart"
|
||||
|
||||
|
||||
class JsonRpcReservedErrorRangeEnd(JsonRpcException):
|
||||
CODE = -32000
|
||||
MESSAGE = "jsonrpcReservedErrorRangeEnd"
|
||||
|
||||
|
||||
class LspReservedErrorRangeStart(JsonRpcException):
|
||||
CODE = -32899
|
||||
MESSAGE = "lspReservedErrorRangeStart"
|
||||
|
||||
|
||||
class LspReservedErrorRangeEnd(JsonRpcException):
|
||||
CODE = -32800
|
||||
MESSAGE = "lspReservedErrorRangeEnd"
|
||||
|
||||
|
||||
class JsonRpcServerError(JsonRpcException):
|
||||
def __init__(self, message, code, data=None):
|
||||
if not _is_server_error_code(code):
|
||||
raise ValueError("Error code should be in range -32099 - -32000")
|
||||
super().__init__(message=message, code=code, data=data)
|
||||
|
||||
@classmethod
|
||||
def supports_code(cls, code):
|
||||
return _is_server_error_code(code)
|
||||
|
||||
|
||||
def _is_server_error_code(code):
|
||||
return -32099 <= code <= -32000
|
||||
|
||||
|
||||
_EXCEPTIONS: Set[Type[JsonRpcException]] = {
|
||||
JsonRpcInternalError,
|
||||
JsonRpcInvalidParams,
|
||||
JsonRpcInvalidRequest,
|
||||
JsonRpcMethodNotFound,
|
||||
JsonRpcParseError,
|
||||
JsonRpcRequestCancelled,
|
||||
JsonRpcServerError,
|
||||
}
|
||||
|
||||
|
||||
class PyglsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CommandAlreadyRegisteredError(PyglsError):
|
||||
def __init__(self, command_name):
|
||||
self.command_name = command_name
|
||||
|
||||
def __repr__(self):
|
||||
return f'Command "{self.command_name}" is already registered.'
|
||||
|
||||
|
||||
class FeatureAlreadyRegisteredError(PyglsError):
|
||||
def __init__(self, feature_name):
|
||||
self.feature_name = feature_name
|
||||
|
||||
def __repr__(self):
|
||||
return f'Feature "{self.feature_name}" is already registered.'
|
||||
|
||||
|
||||
class FeatureRequestError(PyglsError):
|
||||
pass
|
||||
|
||||
|
||||
class FeatureNotificationError(PyglsError):
|
||||
pass
|
||||
|
||||
|
||||
class MethodTypeNotRegisteredError(PyglsError):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return f'"{self.name}" is not added to `pygls.lsp.LSP_METHODS_MAP`.'
|
||||
|
||||
|
||||
class ThreadDecoratorError(PyglsError):
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(PyglsError):
|
||||
def __init__(self, errors=None):
|
||||
self.errors = errors or []
|
||||
|
||||
def __repr__(self):
|
||||
opt_errs = "\n-".join([e for e in self.errors])
|
||||
return f"Missing options: {opt_errs}"
|
||||
@@ -0,0 +1,244 @@
|
||||
############################################################################
|
||||
# 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. #
|
||||
############################################################################
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, Optional, get_type_hints
|
||||
|
||||
from pygls.constants import (
|
||||
ATTR_COMMAND_TYPE,
|
||||
ATTR_EXECUTE_IN_THREAD,
|
||||
ATTR_FEATURE_TYPE,
|
||||
ATTR_REGISTERED_NAME,
|
||||
ATTR_REGISTERED_TYPE,
|
||||
PARAM_LS,
|
||||
)
|
||||
from pygls.exceptions import (
|
||||
CommandAlreadyRegisteredError,
|
||||
FeatureAlreadyRegisteredError,
|
||||
ThreadDecoratorError,
|
||||
ValidationError,
|
||||
)
|
||||
from pygls.lsp import get_method_options_type, is_instance
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def assign_help_attrs(f, reg_name, reg_type):
|
||||
setattr(f, ATTR_REGISTERED_NAME, reg_name)
|
||||
setattr(f, ATTR_REGISTERED_TYPE, reg_type)
|
||||
|
||||
|
||||
def assign_thread_attr(f):
|
||||
setattr(f, ATTR_EXECUTE_IN_THREAD, True)
|
||||
|
||||
|
||||
def get_help_attrs(f):
|
||||
return getattr(f, ATTR_REGISTERED_NAME, None), getattr(
|
||||
f, ATTR_REGISTERED_TYPE, None
|
||||
)
|
||||
|
||||
|
||||
def has_ls_param_or_annotation(f, annotation):
|
||||
"""Returns true if callable has first parameter named `ls` or type of
|
||||
annotation"""
|
||||
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
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_thread_function(f):
|
||||
return getattr(f, ATTR_EXECUTE_IN_THREAD, False)
|
||||
|
||||
|
||||
def wrap_with_server(f, server):
|
||||
"""Returns a new callable/coroutine with server as first argument."""
|
||||
if not has_ls_param_or_annotation(f, type(server)):
|
||||
return f
|
||||
|
||||
if asyncio.iscoroutinefunction(f):
|
||||
|
||||
async def wrapped(*args, **kwargs):
|
||||
return await f(server, *args, **kwargs)
|
||||
|
||||
else:
|
||||
wrapped = functools.partial(f, server)
|
||||
if is_thread_function(f):
|
||||
assign_thread_attr(wrapped)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
class FeatureManager:
|
||||
"""A class for managing server features.
|
||||
|
||||
Attributes:
|
||||
_builtin_features(dict): Predefined set of lsp methods
|
||||
_feature_options(dict): Registered feature's options
|
||||
_features(dict): Registered features
|
||||
_commands(dict): Registered commands
|
||||
server(LanguageServer): Reference to the language server
|
||||
If passed, server will be passed to registered
|
||||
features/commands with first parameter:
|
||||
1. ls - parameter naming convention
|
||||
2. name: LanguageServer - add typings
|
||||
"""
|
||||
|
||||
def __init__(self, server=None, converter=None):
|
||||
self._builtin_features = {}
|
||||
self._feature_options = {}
|
||||
self._features = {}
|
||||
self._commands = {}
|
||||
self.server = server
|
||||
self.converter = converter
|
||||
|
||||
def add_builtin_feature(self, feature_name: str, func: Callable) -> None:
|
||||
"""Registers builtin (predefined) feature."""
|
||||
self._builtin_features[feature_name] = func
|
||||
logger.info("Registered builtin feature %s", feature_name)
|
||||
|
||||
@property
|
||||
def builtin_features(self) -> Dict:
|
||||
"""Returns server builtin features."""
|
||||
return self._builtin_features
|
||||
|
||||
def command(self, command_name: str) -> Callable:
|
||||
"""Decorator used to register custom commands.
|
||||
|
||||
Example:
|
||||
@ls.command('myCustomCommand')
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
# Validate
|
||||
if command_name is None or command_name.strip() == "":
|
||||
logger.error("Missing command name.")
|
||||
raise ValidationError("Command name is required.")
|
||||
|
||||
# Check if not already registered
|
||||
if command_name in self._commands:
|
||||
logger.error('Command "%s" is already registered.', command_name)
|
||||
raise CommandAlreadyRegisteredError(command_name)
|
||||
|
||||
assign_help_attrs(f, command_name, ATTR_COMMAND_TYPE)
|
||||
|
||||
wrapped = wrap_with_server(f, self.server)
|
||||
# Assign help attributes for thread decorator
|
||||
assign_help_attrs(wrapped, command_name, ATTR_COMMAND_TYPE)
|
||||
|
||||
self._commands[command_name] = wrapped
|
||||
|
||||
logger.info('Command "%s" is successfully registered.', command_name)
|
||||
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
@property
|
||||
def commands(self) -> Dict:
|
||||
"""Returns registered custom commands."""
|
||||
return self._commands
|
||||
|
||||
def feature(
|
||||
self,
|
||||
feature_name: str,
|
||||
options: Optional[Any] = None,
|
||||
) -> Callable:
|
||||
"""Decorator used to register LSP features.
|
||||
|
||||
Example:
|
||||
@ls.feature('textDocument/completion', CompletionItems(trigger_characters=['.']))
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
# Validate
|
||||
if feature_name is None or feature_name.strip() == "":
|
||||
logger.error("Missing feature name.")
|
||||
raise ValidationError("Feature name is required.")
|
||||
|
||||
# Add feature if not exists
|
||||
if feature_name in self._features:
|
||||
logger.error('Feature "%s" is already registered.', feature_name)
|
||||
raise FeatureAlreadyRegisteredError(feature_name)
|
||||
|
||||
assign_help_attrs(f, feature_name, ATTR_FEATURE_TYPE)
|
||||
|
||||
wrapped = wrap_with_server(f, self.server)
|
||||
# Assign help attributes for thread decorator
|
||||
assign_help_attrs(wrapped, feature_name, ATTR_FEATURE_TYPE)
|
||||
|
||||
self._features[feature_name] = wrapped
|
||||
|
||||
if options:
|
||||
options_type = get_method_options_type(feature_name)
|
||||
if options_type and not is_instance(
|
||||
self.converter, options, options_type
|
||||
):
|
||||
raise TypeError(
|
||||
(
|
||||
f'Options of method "{feature_name}"'
|
||||
f" should be instance of type {options_type}"
|
||||
)
|
||||
)
|
||||
self._feature_options[feature_name] = options
|
||||
|
||||
logger.info('Registered "%s" with options "%s"', feature_name, options)
|
||||
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
@property
|
||||
def feature_options(self) -> Dict:
|
||||
"""Returns feature options for registered features."""
|
||||
return self._feature_options
|
||||
|
||||
@property
|
||||
def features(self) -> Dict:
|
||||
"""Returns registered features"""
|
||||
return self._features
|
||||
|
||||
def thread(self) -> Callable:
|
||||
"""Decorator that mark function to execute it in a thread."""
|
||||
|
||||
def decorator(f):
|
||||
if asyncio.iscoroutinefunction(f):
|
||||
raise ThreadDecoratorError(
|
||||
f'Thread decorator cannot be used with async functions "{f.__name__}"'
|
||||
)
|
||||
|
||||
# Allow any decorator order
|
||||
try:
|
||||
reg_name = getattr(f, ATTR_REGISTERED_NAME)
|
||||
reg_type = getattr(f, ATTR_REGISTERED_TYPE)
|
||||
|
||||
if reg_type is ATTR_FEATURE_TYPE:
|
||||
assign_thread_attr(self.features[reg_name])
|
||||
elif reg_type is ATTR_COMMAND_TYPE:
|
||||
assign_thread_attr(self.commands[reg_name])
|
||||
|
||||
except AttributeError:
|
||||
assign_thread_attr(f)
|
||||
|
||||
return f
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,139 @@
|
||||
############################################################################
|
||||
# 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. #
|
||||
############################################################################
|
||||
import cattrs
|
||||
from typing import Any, Callable, List, Optional, Union
|
||||
|
||||
from lsprotocol.types import (
|
||||
ALL_TYPES_MAP,
|
||||
METHOD_TO_TYPES,
|
||||
TEXT_DOCUMENT_DID_SAVE,
|
||||
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL,
|
||||
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA,
|
||||
TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE,
|
||||
WORKSPACE_DID_CREATE_FILES,
|
||||
WORKSPACE_DID_DELETE_FILES,
|
||||
WORKSPACE_DID_RENAME_FILES,
|
||||
WORKSPACE_WILL_CREATE_FILES,
|
||||
WORKSPACE_WILL_DELETE_FILES,
|
||||
WORKSPACE_WILL_RENAME_FILES,
|
||||
FileOperationRegistrationOptions,
|
||||
SaveOptions,
|
||||
SemanticTokensLegend,
|
||||
SemanticTokensRegistrationOptions,
|
||||
ShowDocumentResult,
|
||||
)
|
||||
|
||||
from pygls.exceptions import MethodTypeNotRegisteredError
|
||||
|
||||
ConfigCallbackType = Callable[[List[Any]], None]
|
||||
ShowDocumentCallbackType = Callable[[ShowDocumentResult], None]
|
||||
|
||||
METHOD_TO_OPTIONS = {
|
||||
TEXT_DOCUMENT_DID_SAVE: SaveOptions,
|
||||
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL: Union[
|
||||
SemanticTokensLegend, SemanticTokensRegistrationOptions
|
||||
],
|
||||
TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA: Union[
|
||||
SemanticTokensLegend, SemanticTokensRegistrationOptions
|
||||
],
|
||||
TEXT_DOCUMENT_SEMANTIC_TOKENS_RANGE: Union[
|
||||
SemanticTokensLegend, SemanticTokensRegistrationOptions
|
||||
],
|
||||
WORKSPACE_DID_CREATE_FILES: FileOperationRegistrationOptions,
|
||||
WORKSPACE_DID_DELETE_FILES: FileOperationRegistrationOptions,
|
||||
WORKSPACE_DID_RENAME_FILES: FileOperationRegistrationOptions,
|
||||
WORKSPACE_WILL_CREATE_FILES: FileOperationRegistrationOptions,
|
||||
WORKSPACE_WILL_DELETE_FILES: FileOperationRegistrationOptions,
|
||||
WORKSPACE_WILL_RENAME_FILES: FileOperationRegistrationOptions,
|
||||
}
|
||||
|
||||
|
||||
def get_method_registration_options_type(
|
||||
method_name, lsp_methods_map=METHOD_TO_TYPES
|
||||
) -> Optional[Any]:
|
||||
"""The type corresponding with a method's options when dynamically registering
|
||||
capability for it."""
|
||||
|
||||
try:
|
||||
return lsp_methods_map[method_name][3]
|
||||
except KeyError:
|
||||
raise MethodTypeNotRegisteredError(method_name)
|
||||
|
||||
|
||||
def get_method_options_type(
|
||||
method_name, lsp_options_map=METHOD_TO_OPTIONS, lsp_methods_map=METHOD_TO_TYPES
|
||||
) -> Optional[Any]:
|
||||
"""Return the type corresponding with a method's ``ServerCapabilities`` fields.
|
||||
|
||||
In the majority of cases this simply means returning the ``<MethodName>Options``
|
||||
type, which we can easily derive from the method's
|
||||
``<MethodName>RegistrationOptions`` type.
|
||||
|
||||
However, where the options are more involved (such as semantic tokens) and
|
||||
``pygls`` does some extra work to help derive the options for the user the type
|
||||
has to be provided via the ``lsp_options_map``
|
||||
|
||||
Arguments:
|
||||
method_name:
|
||||
The lsp method name to retrieve the options for
|
||||
|
||||
lsp_options_map:
|
||||
The map used to override the default options type finding behavior
|
||||
|
||||
lsp_methods_map:
|
||||
The standard map used to look up the various method types.
|
||||
"""
|
||||
|
||||
options_type = lsp_options_map.get(method_name, None)
|
||||
if options_type is not None:
|
||||
return options_type
|
||||
|
||||
registration_type = get_method_registration_options_type(
|
||||
method_name, lsp_methods_map
|
||||
)
|
||||
if registration_type is None:
|
||||
return None
|
||||
|
||||
type_name = registration_type.__name__.replace("Registration", "")
|
||||
options_type = ALL_TYPES_MAP.get(type_name, None)
|
||||
|
||||
if options_type is None:
|
||||
raise MethodTypeNotRegisteredError(method_name)
|
||||
|
||||
return options_type
|
||||
|
||||
|
||||
def get_method_params_type(method_name, lsp_methods_map=METHOD_TO_TYPES):
|
||||
try:
|
||||
return lsp_methods_map[method_name][2]
|
||||
except KeyError:
|
||||
raise MethodTypeNotRegisteredError(method_name)
|
||||
|
||||
|
||||
def get_method_return_type(method_name, lsp_methods_map=METHOD_TO_TYPES):
|
||||
try:
|
||||
return lsp_methods_map[method_name][1]
|
||||
except KeyError:
|
||||
raise MethodTypeNotRegisteredError(method_name)
|
||||
|
||||
|
||||
def is_instance(cv: cattrs.Converter, o, t):
|
||||
try:
|
||||
cv.unstructure(o, t)
|
||||
return True
|
||||
except TypeError:
|
||||
return False
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
import asyncio
|
||||
from concurrent.futures import Future
|
||||
from typing import Dict
|
||||
|
||||
from lsprotocol.types import (
|
||||
PROGRESS,
|
||||
WINDOW_WORK_DONE_PROGRESS_CREATE,
|
||||
ProgressParams,
|
||||
ProgressToken,
|
||||
WorkDoneProgressBegin,
|
||||
WorkDoneProgressEnd,
|
||||
WorkDoneProgressReport,
|
||||
WorkDoneProgressCreateParams,
|
||||
)
|
||||
from pygls.protocol import LanguageServerProtocol
|
||||
|
||||
|
||||
class Progress:
|
||||
"""A class for working with client's progress bar.
|
||||
|
||||
Attributes:
|
||||
_lsp(LanguageServerProtocol): Language server protocol instance
|
||||
tokens(dict): Holds futures for work done progress tokens that are
|
||||
already registered. These futures will be cancelled if the client
|
||||
sends a cancel work done process notification.
|
||||
"""
|
||||
|
||||
def __init__(self, lsp: LanguageServerProtocol) -> None:
|
||||
self._lsp = lsp
|
||||
|
||||
self.tokens: Dict[ProgressToken, Future] = {}
|
||||
|
||||
def _check_token_registered(self, token: ProgressToken) -> None:
|
||||
if token in self.tokens:
|
||||
raise Exception("Token is already registered!")
|
||||
|
||||
def _register_token(self, token: ProgressToken) -> None:
|
||||
self.tokens[token] = Future()
|
||||
|
||||
def create(self, token: ProgressToken, callback=None) -> Future:
|
||||
"""Create a server initiated work done progress."""
|
||||
self._check_token_registered(token)
|
||||
|
||||
def on_created(*args, **kwargs):
|
||||
self._register_token(token)
|
||||
if callback is not None:
|
||||
callback(*args, **kwargs)
|
||||
|
||||
return self._lsp.send_request(
|
||||
WINDOW_WORK_DONE_PROGRESS_CREATE,
|
||||
WorkDoneProgressCreateParams(token=token),
|
||||
on_created,
|
||||
)
|
||||
|
||||
async def create_async(self, token: ProgressToken) -> asyncio.Future:
|
||||
"""Create a server initiated work done progress."""
|
||||
self._check_token_registered(token)
|
||||
|
||||
result = await self._lsp.send_request_async(
|
||||
WINDOW_WORK_DONE_PROGRESS_CREATE,
|
||||
WorkDoneProgressCreateParams(token=token),
|
||||
)
|
||||
self._register_token(token)
|
||||
return result
|
||||
|
||||
def begin(self, token: ProgressToken, value: WorkDoneProgressBegin) -> None:
|
||||
"""Notify beginning of work."""
|
||||
# Register cancellation future for the case of client initiated progress
|
||||
self.tokens.setdefault(token, Future())
|
||||
|
||||
return self._lsp.notify(PROGRESS, ProgressParams(token=token, value=value))
|
||||
|
||||
def report(self, token: ProgressToken, value: WorkDoneProgressReport) -> None:
|
||||
"""Notify progress of work."""
|
||||
self._lsp.notify(PROGRESS, ProgressParams(token=token, value=value))
|
||||
|
||||
def end(self, token: ProgressToken, value: WorkDoneProgressEnd) -> None:
|
||||
"""Notify end of work."""
|
||||
self._lsp.notify(PROGRESS, ProgressParams(token=token, value=value))
|
||||
@@ -0,0 +1,78 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from lsprotocol import converters
|
||||
|
||||
from pygls.protocol.json_rpc import (
|
||||
JsonRPCNotification,
|
||||
JsonRPCProtocol,
|
||||
JsonRPCRequestMessage,
|
||||
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):
|
||||
"""Create nested objects (namedtuple) from dict."""
|
||||
|
||||
if d is None:
|
||||
return None
|
||||
|
||||
if not isinstance(d, dict):
|
||||
return d
|
||||
|
||||
type_name = d.pop("type_name", "Object")
|
||||
return json.loads(
|
||||
json.dumps(d),
|
||||
object_hook=lambda p: namedtuple(type_name, p.keys(), rename=True)(*p.values()),
|
||||
)
|
||||
|
||||
|
||||
def _params_field_structure_hook(obj, cls):
|
||||
if "params" in obj:
|
||||
obj["params"] = _dict_to_object(obj["params"])
|
||||
|
||||
return cls(**obj)
|
||||
|
||||
|
||||
def _result_field_structure_hook(obj, cls):
|
||||
if "result" in obj:
|
||||
obj["result"] = _dict_to_object(obj["result"])
|
||||
|
||||
return cls(**obj)
|
||||
|
||||
|
||||
def default_converter():
|
||||
"""Default converter factory function."""
|
||||
|
||||
converter = converters.get_converter()
|
||||
converter.register_structure_hook(
|
||||
JsonRPCRequestMessage, _params_field_structure_hook
|
||||
)
|
||||
|
||||
converter.register_structure_hook(
|
||||
JsonRPCResponseMessage, _result_field_structure_hook
|
||||
)
|
||||
|
||||
converter.register_structure_hook(JsonRPCNotification, _params_field_structure_hook)
|
||||
|
||||
return converter
|
||||
|
||||
|
||||
__all__ = (
|
||||
"JsonRPCProtocol",
|
||||
"LanguageServerProtocol",
|
||||
"JsonRPCRequestMessage",
|
||||
"JsonRPCResponseMessage",
|
||||
"JsonRPCNotification",
|
||||
"LSPMeta",
|
||||
"call_user_feature",
|
||||
"_dict_to_object",
|
||||
"_params_field_structure_hook",
|
||||
"_result_field_structure_hook",
|
||||
"default_converter",
|
||||
"lsp_method",
|
||||
)
|
||||
@@ -0,0 +1,560 @@
|
||||
############################################################################
|
||||
# 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 enum
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import traceback
|
||||
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
|
||||
|
||||
|
||||
import attrs
|
||||
from cattrs.errors import ClassValidationError
|
||||
|
||||
from lsprotocol.types import (
|
||||
CANCEL_REQUEST,
|
||||
EXIT,
|
||||
WORKSPACE_EXECUTE_COMMAND,
|
||||
ResponseError,
|
||||
ResponseErrorMessage,
|
||||
)
|
||||
|
||||
from pygls.exceptions import (
|
||||
JsonRpcException,
|
||||
JsonRpcInternalError,
|
||||
JsonRpcInvalidParams,
|
||||
JsonRpcMethodNotFound,
|
||||
JsonRpcRequestCancelled,
|
||||
FeatureNotificationError,
|
||||
FeatureRequestError,
|
||||
)
|
||||
from pygls.feature_manager import FeatureManager, is_thread_function
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@attrs.define
|
||||
class JsonRPCNotification:
|
||||
"""A class that represents a generic json rpc notification message.
|
||||
Used as a fallback for unknown types.
|
||||
"""
|
||||
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
|
||||
|
||||
@attrs.define
|
||||
class JsonRPCRequestMessage:
|
||||
"""A class that represents a generic json rpc request message.
|
||||
Used as a fallback for unknown types.
|
||||
"""
|
||||
|
||||
id: Union[int, str]
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
|
||||
|
||||
@attrs.define
|
||||
class JsonRPCResponseMessage:
|
||||
"""A class that represents a generic json rpc response message.
|
||||
Used as a fallback for unknown types.
|
||||
"""
|
||||
|
||||
id: Union[int, str]
|
||||
jsonrpc: str
|
||||
result: Any
|
||||
|
||||
|
||||
class JsonRPCProtocol(asyncio.Protocol):
|
||||
"""Json RPC protocol implementation using on top of `asyncio.Protocol`.
|
||||
|
||||
Specification of the protocol can be found here:
|
||||
https://www.jsonrpc.org/specification
|
||||
|
||||
This class provides bidirectional communication which is needed for LSP.
|
||||
"""
|
||||
|
||||
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):
|
||||
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.fm = FeatureManager(server, converter)
|
||||
self.transport: Optional[
|
||||
Union[asyncio.WriteTransport, WebSocketTransportAdapter]
|
||||
] = None
|
||||
self._message_buf: List[bytes] = []
|
||||
|
||||
self._send_only_body = 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)
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
def _execute_request(self, msg_id, handler, params):
|
||||
"""Executes request message handler."""
|
||||
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
future = asyncio.ensure_future(handler(params))
|
||||
self._request_futures[msg_id] = future
|
||||
future.add_done_callback(partial(self._execute_request_callback, msg_id))
|
||||
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))
|
||||
|
||||
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())
|
||||
else:
|
||||
self._send_response(
|
||||
msg_id,
|
||||
error=JsonRpcRequestCancelled(
|
||||
f'Request with id "{msg_id}" is canceled'
|
||||
).to_response_error(),
|
||||
)
|
||||
self._request_futures.pop(msg_id, None)
|
||||
except Exception:
|
||||
error = JsonRpcInternalError.of(sys.exc_info())
|
||||
logger.exception('Exception occurred for message "%s": %s', msg_id, error)
|
||||
self._send_response(msg_id, error=error.to_response_error())
|
||||
|
||||
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 _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:
|
||||
try:
|
||||
return self.fm.features[feature_name]
|
||||
except KeyError:
|
||||
raise JsonRpcMethodNotFound.of(feature_name)
|
||||
|
||||
def _handle_cancel_notification(self, msg_id):
|
||||
"""Handles a cancel notification from the client."""
|
||||
future = self._request_futures.pop(msg_id, None)
|
||||
|
||||
if not future:
|
||||
logger.warning('Cancel notification for unknown message id "%s"', msg_id)
|
||||
return
|
||||
|
||||
# Will only work if the request hasn't started executing
|
||||
if future.cancel():
|
||||
logger.info('Cancelled request with id "%s"', msg_id)
|
||||
|
||||
def _handle_notification(self, method_name, params):
|
||||
"""Handles a notification from the client."""
|
||||
if method_name == CANCEL_REQUEST:
|
||||
self._handle_cancel_notification(params.id)
|
||||
return
|
||||
|
||||
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)
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
'Failed to handle notification "%s": %s',
|
||||
method_name,
|
||||
params,
|
||||
exc_info=True,
|
||||
)
|
||||
self._server._report_server_error(error, FeatureNotificationError)
|
||||
|
||||
def _handle_request(self, msg_id, method_name, params):
|
||||
"""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)
|
||||
|
||||
except JsonRpcException as error:
|
||||
logger.exception(
|
||||
"Failed to handle request %s %s %s",
|
||||
msg_id,
|
||||
method_name,
|
||||
params,
|
||||
exc_info=True,
|
||||
)
|
||||
self._send_response(msg_id, None, error.to_response_error())
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
"Failed to handle request %s %s %s",
|
||||
msg_id,
|
||||
method_name,
|
||||
params,
|
||||
exc_info=True,
|
||||
)
|
||||
err = JsonRpcInternalError.of(sys.exc_info()).to_response_error()
|
||||
self._send_response(msg_id, None, err)
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
|
||||
def _handle_response(self, msg_id, result=None, error=None):
|
||||
"""Handles a response from the client."""
|
||||
future = self._request_futures.pop(msg_id, None)
|
||||
|
||||
if not future:
|
||||
logger.warning('Received response to unknown message id "%s"', msg_id)
|
||||
return
|
||||
|
||||
if error is not None:
|
||||
logger.debug('Received error response to message "%s": %s', msg_id, error)
|
||||
future.set_exception(JsonRpcException.from_error(error))
|
||||
else:
|
||||
logger.debug('Received result for message "%s": %s', msg_id, result)
|
||||
future.set_result(result)
|
||||
|
||||
def _serialize_message(self, data):
|
||||
"""Function used to serialize data sent to the client."""
|
||||
|
||||
if hasattr(data, "__attrs_attrs__"):
|
||||
return self._converter.unstructure(data)
|
||||
|
||||
if isinstance(data, enum.Enum):
|
||||
return data.value
|
||||
|
||||
return data.__dict__
|
||||
|
||||
def _deserialize_message(self, data):
|
||||
"""Function used to deserialize data recevied from the client."""
|
||||
|
||||
if "jsonrpc" not in data:
|
||||
return data
|
||||
|
||||
try:
|
||||
if "id" in data:
|
||||
if "error" in data:
|
||||
return self._converter.structure(data, ResponseErrorMessage)
|
||||
elif "method" in data:
|
||||
request_type = (
|
||||
self.get_message_type(data["method"]) or JsonRPCRequestMessage
|
||||
)
|
||||
return self._converter.structure(data, request_type)
|
||||
else:
|
||||
response_type = (
|
||||
self._result_types.pop(data["id"]) or JsonRPCResponseMessage
|
||||
)
|
||||
return self._converter.structure(data, response_type)
|
||||
|
||||
else:
|
||||
method = data.get("method", "")
|
||||
notification_type = self.get_message_type(method) or JsonRPCNotification
|
||||
return self._converter.structure(data, notification_type)
|
||||
|
||||
except ClassValidationError as exc:
|
||||
logger.error("Unable to deserialize message\n%s", traceback.format_exc())
|
||||
raise JsonRpcInvalidParams() from exc
|
||||
|
||||
except Exception as exc:
|
||||
logger.error("Unable to deserialize message\n%s", traceback.format_exc())
|
||||
raise JsonRpcInternalError() from exc
|
||||
|
||||
def _procedure_handler(self, message):
|
||||
"""Delegates message to handlers depending on message type."""
|
||||
|
||||
if message.jsonrpc != JsonRPCProtocol.VERSION:
|
||||
logger.warning('Unknown message "%s"', message)
|
||||
return
|
||||
|
||||
if self._shutdown and getattr(message, "method", "") != EXIT:
|
||||
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)
|
||||
|
||||
def _send_data(self, data):
|
||||
"""Sends data to the client."""
|
||||
if not data:
|
||||
return
|
||||
|
||||
if self.transport is None:
|
||||
logger.error("Unable to send data, no available transport!")
|
||||
return
|
||||
|
||||
try:
|
||||
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
|
||||
|
||||
header = (
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
|
||||
).encode(self.CHARSET)
|
||||
|
||||
self.transport.write(header + body.encode(self.CHARSET))
|
||||
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
|
||||
):
|
||||
"""Sends a JSON RPC response to the client.
|
||||
|
||||
Args:
|
||||
msg_id(str): Id from request
|
||||
result(any): Result returned by handler
|
||||
error(any): Error returned by handler
|
||||
"""
|
||||
|
||||
if error is not None:
|
||||
response = ResponseErrorMessage(id=msg_id, error=error)
|
||||
|
||||
else:
|
||||
response_type = self._result_types.pop(msg_id, JsonRPCResponseMessage)
|
||||
response = response_type(
|
||||
id=msg_id, result=result, jsonrpc=JsonRPCProtocol.VERSION
|
||||
)
|
||||
|
||||
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
|
||||
self,
|
||||
transport: asyncio.Transport,
|
||||
):
|
||||
"""Method from base class, called when connection is established"""
|
||||
self.transport = transport
|
||||
|
||||
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)
|
||||
|
||||
def _data_received(self, data: bytes):
|
||||
"""Method from base class, called when server receives the data"""
|
||||
logger.debug("Received %r", data)
|
||||
|
||||
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]:
|
||||
"""Return the type definition of the message associated with the given method."""
|
||||
return None
|
||||
|
||||
def get_result_type(self, method: str) -> Optional[Type]:
|
||||
"""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."""
|
||||
|
||||
logger.debug("Sending notification: '%s' %s", method, params)
|
||||
|
||||
notification_type = self.get_message_type(method) or JsonRPCNotification
|
||||
notification = notification_type(
|
||||
method=method, params=params, jsonrpc=JsonRPCProtocol.VERSION
|
||||
)
|
||||
|
||||
self._send_data(notification)
|
||||
|
||||
def send_request(self, method, params=None, callback=None, msg_id=None):
|
||||
"""Sends a JSON RPC request to the client.
|
||||
|
||||
Args:
|
||||
method(str): The method name of the message to send
|
||||
params(any): The payload of the message
|
||||
|
||||
Returns:
|
||||
Future that will be resolved once a response has been received
|
||||
"""
|
||||
|
||||
if msg_id is None:
|
||||
msg_id = str(uuid.uuid4())
|
||||
|
||||
request_type = self.get_message_type(method) or JsonRPCRequestMessage
|
||||
logger.debug('Sending request with id "%s": %s %s', msg_id, method, params)
|
||||
|
||||
request = request_type(
|
||||
id=msg_id,
|
||||
method=method,
|
||||
params=params,
|
||||
jsonrpc=JsonRPCProtocol.VERSION,
|
||||
)
|
||||
|
||||
future = Future() # type: ignore[var-annotated]
|
||||
# If callback function is given, call it when result is received
|
||||
if callback:
|
||||
|
||||
def wrapper(future: Future):
|
||||
result = future.result()
|
||||
logger.info("Client response for %s received: %s", params, result)
|
||||
callback(result)
|
||||
|
||||
future.add_done_callback(wrapper)
|
||||
|
||||
self._request_futures[msg_id] = future
|
||||
self._result_types[msg_id] = self.get_result_type(method)
|
||||
|
||||
self._send_data(request)
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,569 @@
|
||||
############################################################################
|
||||
# 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 sys
|
||||
from concurrent.futures import Future
|
||||
from functools import lru_cache
|
||||
from itertools import zip_longest
|
||||
from typing import (
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
|
||||
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.protocol.json_rpc import JsonRPCProtocol
|
||||
from pygls.protocol.lsp_meta import LSPMeta
|
||||
from pygls.uris import from_fs_path
|
||||
from pygls.workspace import Workspace
|
||||
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def lsp_method(method_name: str) -> Callable[[F], F]:
|
||||
def decorator(f: F) -> F:
|
||||
f.method_name = method_name # type: ignore[attr-defined]
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
"""A class that represents language server protocol.
|
||||
|
||||
It contains implementations for generic LSP features.
|
||||
|
||||
Attributes:
|
||||
workspace(Workspace): In memory workspace
|
||||
"""
|
||||
|
||||
def __init__(self, server, converter):
|
||||
super().__init__(server, converter)
|
||||
|
||||
self._workspace: Optional[Workspace] = None
|
||||
self.trace = None
|
||||
|
||||
from pygls.progress import Progress
|
||||
|
||||
self.progress = Progress(self)
|
||||
|
||||
self.server_info = InitializeResultServerInfoType(
|
||||
name=server.name,
|
||||
version=server.version,
|
||||
)
|
||||
|
||||
self._register_builtin_features()
|
||||
|
||||
def _register_builtin_features(self):
|
||||
"""Registers generic LSP features from this class."""
|
||||
for name in dir(self):
|
||||
if name in {"workspace"}:
|
||||
continue
|
||||
|
||||
attr = getattr(self, name)
|
||||
if callable(attr) and hasattr(attr, "method_name"):
|
||||
self.fm.add_builtin_feature(attr.method_name, attr)
|
||||
|
||||
@property
|
||||
def workspace(self) -> Workspace:
|
||||
if self._workspace is None:
|
||||
raise RuntimeError(
|
||||
"The workspace is not available - has the server been initialized?"
|
||||
)
|
||||
|
||||
return self._workspace
|
||||
|
||||
@lru_cache()
|
||||
def get_message_type(self, method: str) -> Optional[Type]:
|
||||
"""Return LSP type definitions, as provided by `lsprotocol`"""
|
||||
return 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 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:
|
||||
"""Stops the server process."""
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
|
||||
sys.exit(0 if self._shutdown else 1)
|
||||
|
||||
@lsp_method(INITIALIZE)
|
||||
def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
|
||||
"""Method that initializes language server.
|
||||
It will compute and return server capabilities based on
|
||||
registered features.
|
||||
"""
|
||||
logger.info("Language server initialized %s", params)
|
||||
|
||||
self._server.process_id = params.process_id
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
root_path = params.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,
|
||||
self.server_capabilities.position_encoding,
|
||||
)
|
||||
|
||||
self.trace = TraceValues.Off
|
||||
|
||||
return InitializeResult(
|
||||
capabilities=self.server_capabilities,
|
||||
server_info=self.server_info,
|
||||
)
|
||||
|
||||
@lsp_method(INITIALIZED)
|
||||
def lsp_initialized(self, *args) -> None:
|
||||
"""Notification received when client and server are connected."""
|
||||
pass
|
||||
|
||||
@lsp_method(SHUTDOWN)
|
||||
def lsp_shutdown(self, *args) -> None:
|
||||
"""Request from client which asks server to shutdown."""
|
||||
for future in self._request_futures.values():
|
||||
future.cancel()
|
||||
|
||||
self._shutdown = True
|
||||
return None
|
||||
|
||||
@lsp_method(TEXT_DOCUMENT_DID_CHANGE)
|
||||
def lsp_text_document__did_change(
|
||||
self, params: DidChangeTextDocumentParams
|
||||
) -> None:
|
||||
"""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:
|
||||
"""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:
|
||||
"""Puts document to the workspace."""
|
||||
self.workspace.put_text_document(params.text_document)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_OPEN)
|
||||
def lsp_notebook_document__did_open(
|
||||
self, params: DidOpenNotebookDocumentParams
|
||||
) -> None:
|
||||
"""Put a notebook document into the workspace"""
|
||||
self.workspace.put_notebook_document(params)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_CHANGE)
|
||||
def lsp_notebook_document__did_change(
|
||||
self, params: DidChangeNotebookDocumentParams
|
||||
) -> None:
|
||||
"""Update a notebook's contents"""
|
||||
self.workspace.update_notebook_document(params)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_CLOSE)
|
||||
def lsp_notebook_document__did_close(
|
||||
self, params: DidCloseNotebookDocumentParams
|
||||
) -> None:
|
||||
"""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:
|
||||
"""Changes server trace value."""
|
||||
self.trace = params.value
|
||||
|
||||
@lsp_method(WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS)
|
||||
def lsp_workspace__did_change_workspace_folders(
|
||||
self, params: DidChangeWorkspaceFoldersParams
|
||||
) -> None:
|
||||
"""Adds/Removes folders from the workspace."""
|
||||
logger.info("Workspace folders changed: %s", params)
|
||||
|
||||
added_folders = params.event.added or []
|
||||
removed_folders = params.event.removed or []
|
||||
|
||||
for f_add, f_remove in zip_longest(added_folders, removed_folders):
|
||||
if f_add:
|
||||
self.workspace.add_folder(f_add)
|
||||
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)
|
||||
|
||||
@lsp_method(WINDOW_WORK_DONE_PROGRESS_CANCEL)
|
||||
def lsp_work_done_progress_cancel(
|
||||
self, params: WorkDoneProgressCancelParams
|
||||
) -> None:
|
||||
"""Received a progress cancellation from client."""
|
||||
future = self.progress.tokens.get(params.token)
|
||||
if future is None:
|
||||
logger.warning(
|
||||
"Ignoring work done progress cancel for unknown token %s", params.token
|
||||
)
|
||||
else:
|
||||
future.cancel()
|
||||
|
||||
def get_configuration(
|
||||
self,
|
||||
params: WorkspaceConfigurationParams,
|
||||
callback: Optional[ConfigCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Sends configuration request to the client.
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
params(WorkspaceConfigurationParams): WorkspaceConfigurationParams from lsp specs
|
||||
Returns:
|
||||
asyncio.Future that can be awaited
|
||||
"""
|
||||
return asyncio.wrap_future(self.get_configuration(params))
|
||||
|
||||
def log_trace(self, message: str, verbose: Optional[str] = None) -> None:
|
||||
"""Sends trace notification to the client."""
|
||||
if self.trace == TraceValues.Off:
|
||||
return
|
||||
|
||||
params = LogTraceParams(message=message)
|
||||
if verbose and self.trace == TraceValues.Verbose:
|
||||
params.verbose = verbose
|
||||
|
||||
self.notify(LOG_TRACE, params)
|
||||
|
||||
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)
|
||||
|
||||
params = self._construct_publish_diagnostic_type(
|
||||
params_or_uri, diagnostics, version, **kwargs
|
||||
)
|
||||
else:
|
||||
params = params_or_uri
|
||||
return params
|
||||
|
||||
def _construct_publish_diagnostic_type(
|
||||
self,
|
||||
uri: str,
|
||||
diagnostics: Optional[List[Diagnostic]],
|
||||
version: Optional[int],
|
||||
**kwargs,
|
||||
) -> PublishDiagnosticsParams:
|
||||
if diagnostics is None:
|
||||
diagnostics = []
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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 unregister_capability_async(
|
||||
self, params: UnregistrationParams
|
||||
) -> asyncio.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:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.unregister_capability(params, None))
|
||||
@@ -0,0 +1,51 @@
|
||||
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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# Marker file for PEP 561. The pygls package uses inline types.
|
||||
|
||||
@@ -0,0 +1,616 @@
|
||||
############################################################################
|
||||
# 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. #
|
||||
############################################################################
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from concurrent.futures import Future, 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
|
||||
|
||||
|
||||
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:
|
||||
"""Base server class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
protocol_cls
|
||||
Protocol implementation that must be 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`
|
||||
|
||||
"""
|
||||
|
||||
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,
|
||||
):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown server."""
|
||||
logger.info("Shutting down the server")
|
||||
|
||||
if self._stop_event is not None:
|
||||
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()
|
||||
|
||||
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 start_io(self, stdin: Optional[TextIO] = None, stdout: Optional[TextIO] = None):
|
||||
"""Starts IO server."""
|
||||
logger.info("Starting 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]
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
except BrokenPipeError:
|
||||
logger.error("Connection to the client is lost! Shutting down the server.")
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
def start_pyodide(self):
|
||||
logger.info("Starting Pyodide 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
|
||||
|
||||
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.shutdown()
|
||||
|
||||
def start_ws(self, host: str, port: int) -> None:
|
||||
"""Starts WebSocket server."""
|
||||
try:
|
||||
from websockets.server import serve
|
||||
except ImportError:
|
||||
logger.error("Run `pip install pygls[ws]` to install `websockets`.")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Starting WebSocket server on {}:{}".format(host, port))
|
||||
|
||||
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()
|
||||
self.shutdown()
|
||||
|
||||
if not IS_PYODIDE:
|
||||
|
||||
@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)
|
||||
|
||||
return self._thread_pool
|
||||
|
||||
@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)
|
||||
|
||||
def command(self, command_name: str) -> Callable[[F], F]:
|
||||
"""Decorator used to register custom commands.
|
||||
|
||||
Example
|
||||
-------
|
||||
::
|
||||
|
||||
@ls.command('myCustomCommand')
|
||||
def my_cmd(ls, a, b, c):
|
||||
pass
|
||||
"""
|
||||
return self.lsp.fm.command(command_name)
|
||||
|
||||
@property
|
||||
def client_capabilities(self) -> ClientCapabilities:
|
||||
"""The client's capabilities."""
|
||||
return self.lsp.client_capabilities
|
||||
|
||||
def feature(
|
||||
self,
|
||||
feature_name: str,
|
||||
options: Optional[Any] = None,
|
||||
) -> Callable[[F], F]:
|
||||
"""Decorator used to register LSP features.
|
||||
|
||||
Example
|
||||
-------
|
||||
::
|
||||
|
||||
@ls.feature('textDocument/completion', CompletionOptions(trigger_characters=['.']))
|
||||
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)
|
||||
|
||||
@property
|
||||
def progress(self) -> Progress:
|
||||
"""Gets the object to manage client's progress bar."""
|
||||
return self.lsp.progress
|
||||
|
||||
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
|
||||
@@ -0,0 +1,184 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2017 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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 collection of URI utilities with logic built on the VSCode URI library.
|
||||
|
||||
https://github.com/Microsoft/vscode-uri/blob/e59cab84f5df6265aed18ae5f43552d3eef13bb9/lib/index.ts
|
||||
"""
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import re
|
||||
from urllib import parse
|
||||
|
||||
from pygls import IS_WIN
|
||||
|
||||
RE_DRIVE_LETTER_PATH = re.compile(r"^\/[a-zA-Z]:")
|
||||
|
||||
URLParts = Tuple[str, str, str, str, str, str]
|
||||
|
||||
|
||||
def _normalize_win_path(path: str):
|
||||
netloc = ""
|
||||
|
||||
# normalize to fwd-slashes on windows,
|
||||
# on other systems bwd-slashes are valid
|
||||
# filename character, eg /f\oo/ba\r.txt
|
||||
if IS_WIN:
|
||||
path = path.replace("\\", "/")
|
||||
|
||||
# check for authority as used in UNC shares
|
||||
# or use the path as given
|
||||
if path[:2] == "//":
|
||||
idx = path.index("/", 2)
|
||||
if idx == -1:
|
||||
netloc = path[2:]
|
||||
else:
|
||||
netloc = path[2:idx]
|
||||
path = path[idx:]
|
||||
|
||||
# Ensure that path starts with a slash
|
||||
# or that it is at least a slash
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
|
||||
# Normalize drive paths to lower case
|
||||
if RE_DRIVE_LETTER_PATH.match(path):
|
||||
path = path[0] + path[1].lower() + path[2:]
|
||||
|
||||
return path, netloc
|
||||
|
||||
|
||||
def from_fs_path(path: str):
|
||||
"""Returns a URI for the given filesystem path."""
|
||||
try:
|
||||
scheme = "file"
|
||||
params, query, fragment = "", "", ""
|
||||
path, netloc = _normalize_win_path(path)
|
||||
return urlunparse((scheme, netloc, path, params, query, fragment))
|
||||
except (AttributeError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def to_fs_path(uri: str):
|
||||
"""
|
||||
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":
|
||||
# unc path: file://shares/c$/far/boo
|
||||
value = f"//{netloc}{path}"
|
||||
|
||||
elif RE_DRIVE_LETTER_PATH.match(path):
|
||||
# windows drive letter: file:///C:/far/boo
|
||||
value = path[1].lower() + path[2:]
|
||||
|
||||
else:
|
||||
# Other path
|
||||
value = path
|
||||
|
||||
if IS_WIN:
|
||||
value = value.replace("/", "\\")
|
||||
|
||||
return value
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
|
||||
def uri_scheme(uri: str):
|
||||
try:
|
||||
return urlparse(uri)[0]
|
||||
except (TypeError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
# TODO: Use `URLParts` type
|
||||
def uri_with(
|
||||
uri: str,
|
||||
scheme: Optional[str] = None,
|
||||
netloc: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
params: Optional[str] = None,
|
||||
query: Optional[str] = None,
|
||||
fragment: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Return a URI with the given part(s) replaced.
|
||||
Parts are decoded / encoded.
|
||||
"""
|
||||
old_scheme, old_netloc, old_path, old_params, old_query, old_fragment = urlparse(
|
||||
uri
|
||||
)
|
||||
|
||||
if path is None:
|
||||
raise Exception("`path` must not be None")
|
||||
|
||||
path, _ = _normalize_win_path(path)
|
||||
return urlunparse(
|
||||
(
|
||||
scheme or old_scheme,
|
||||
netloc or old_netloc,
|
||||
path or old_path,
|
||||
params or old_params,
|
||||
query or old_query,
|
||||
fragment or old_fragment,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def urlparse(uri: str):
|
||||
"""Parse and decode the parts of a URI."""
|
||||
scheme, netloc, path, params, query, fragment = parse.urlparse(uri)
|
||||
return (
|
||||
parse.unquote(scheme),
|
||||
parse.unquote(netloc),
|
||||
parse.unquote(path),
|
||||
parse.unquote(params),
|
||||
parse.unquote(query),
|
||||
parse.unquote(fragment),
|
||||
)
|
||||
|
||||
|
||||
def urlunparse(parts: URLParts) -> str:
|
||||
"""Unparse and encode parts of a URI."""
|
||||
scheme, netloc, path, params, query, fragment = parts
|
||||
|
||||
# Avoid encoding the windows drive letter colon
|
||||
if RE_DRIVE_LETTER_PATH.match(path):
|
||||
quoted_path = path[:3] + parse.quote(path[3:])
|
||||
else:
|
||||
quoted_path = parse.quote(path)
|
||||
|
||||
return parse.urlunparse(
|
||||
(
|
||||
parse.quote(scheme),
|
||||
parse.quote(netloc),
|
||||
quoted_path,
|
||||
parse.quote(params),
|
||||
parse.quote(query),
|
||||
parse.quote(fragment),
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
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)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Workspace",
|
||||
"TextDocument",
|
||||
"PositionCodec",
|
||||
"Document",
|
||||
"utf16_unit_offset",
|
||||
"utf16_num_units",
|
||||
"position_from_utf16",
|
||||
"position_to_utf16",
|
||||
"range_from_utf16",
|
||||
"range_to_utf16",
|
||||
)
|
||||
@@ -0,0 +1,206 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2017 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
import logging
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PositionCodec:
|
||||
def __init__(
|
||||
self,
|
||||
encoding: Optional[
|
||||
Union[types.PositionEncodingKind, str]
|
||||
] = types.PositionEncodingKind.Utf16,
|
||||
):
|
||||
self.encoding = encoding
|
||||
|
||||
@classmethod
|
||||
def is_char_beyond_multilingual_plane(cls, char: str) -> bool:
|
||||
return ord(char) > 0xFFFF
|
||||
|
||||
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 position_from_client_units(
|
||||
self, lines: List[str], position: types.Position
|
||||
) -> types.Position:
|
||||
"""
|
||||
Convert the position.character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
A python application can't use the character member of `Position`
|
||||
directly. As per specification it is represented as a zero-based line and
|
||||
character offset based on posible a UTF-[32|16|8] string representation.
|
||||
|
||||
All characters whose code point exceeds the Basic Multilingual Plane are
|
||||
represented by 2 UTF-16 or 4 UTF-8 code units.
|
||||
|
||||
The offset of the closing quotation mark in x="😋" is
|
||||
- 7 in UTF-8 representation
|
||||
- 5 in UTF-16 representation
|
||||
- 4 in UTF-32 representation
|
||||
|
||||
see: https://github.com/microsoft/language-server-protocol/issues/376
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
|
||||
Returns:
|
||||
The position with `character` being converted to UTF-32 code units.
|
||||
"""
|
||||
if len(lines) == 0:
|
||||
return types.Position(0, 0)
|
||||
if position.line >= len(lines):
|
||||
return types.Position(len(lines) - 1, self.client_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)
|
||||
|
||||
if _client_len == 0:
|
||||
return types.Position(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
|
||||
|
||||
_client_index = 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:
|
||||
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
|
||||
utf32_index += 1
|
||||
|
||||
position = types.Position(line=position.line, character=utf32_index)
|
||||
return position
|
||||
|
||||
def position_to_client_units(
|
||||
self, lines: List[str], position: 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):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-32 code units.
|
||||
|
||||
Returns:
|
||||
The position with `character` being converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
try:
|
||||
character = self.client_num_units(
|
||||
lines[position.line][: position.character]
|
||||
)
|
||||
return types.Position(
|
||||
line=position.line,
|
||||
character=character,
|
||||
)
|
||||
except IndexError:
|
||||
return types.Position(line=len(lines), character=0)
|
||||
|
||||
def range_from_client_units(
|
||||
self, lines: List[str], range: types.Range
|
||||
) -> types.Range:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-32 code units.
|
||||
"""
|
||||
range_new = types.Range(
|
||||
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
|
||||
) -> types.Range:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-32 to UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in code units.
|
||||
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
return types.Range(
|
||||
start=self.position_to_client_units(lines, range.start),
|
||||
end=self.position_to_client_units(lines, range.end),
|
||||
)
|
||||
@@ -0,0 +1,238 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2017 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, Optional, Pattern
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.uris import to_fs_path
|
||||
from .position_codec import PositionCodec
|
||||
|
||||
# TODO: this is not the best e.g. we capture numbers
|
||||
RE_END_WORD = re.compile("^[A-Za-z_0-9]*")
|
||||
RE_START_WORD = re.compile("[A-Za-z_0-9]*$")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextDocument(object):
|
||||
def __init__(
|
||||
self,
|
||||
uri: str,
|
||||
source: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
language_id: Optional[str] = None,
|
||||
local: bool = True,
|
||||
sync_kind: types.TextDocumentSyncKind = types.TextDocumentSyncKind.Incremental,
|
||||
position_codec: Optional[PositionCodec] = None,
|
||||
):
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
path = to_fs_path(uri)
|
||||
if path is None:
|
||||
raise Exception("`path` cannot be None")
|
||||
self.path = path
|
||||
self.language_id = language_id
|
||||
self.filename: Optional[str] = os.path.basename(self.path)
|
||||
|
||||
self._local = local
|
||||
self._source = source
|
||||
|
||||
self._is_sync_kind_full = sync_kind == types.TextDocumentSyncKind.Full
|
||||
self._is_sync_kind_incremental = (
|
||||
sync_kind == types.TextDocumentSyncKind.Incremental
|
||||
)
|
||||
self._is_sync_kind_none = sync_kind == types.TextDocumentSyncKind.None_
|
||||
|
||||
self._position_codec = position_codec if position_codec else PositionCodec()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.uri)
|
||||
|
||||
@property
|
||||
def position_codec(self) -> PositionCodec:
|
||||
return self._position_codec
|
||||
|
||||
def _apply_incremental_change(
|
||||
self, change: types.TextDocumentContentChangeEvent_Type1
|
||||
) -> None:
|
||||
"""Apply an ``Incremental`` text change to the document"""
|
||||
lines = self.lines
|
||||
text = change.text
|
||||
change_range = change.range
|
||||
|
||||
range = self._position_codec.range_from_client_units(lines, change_range)
|
||||
start_line = range.start.line
|
||||
start_col = range.start.character
|
||||
end_line = range.end.line
|
||||
end_col = range.end.character
|
||||
|
||||
# Check for an edit occurring at the very end of the file
|
||||
if start_line == len(lines):
|
||||
self._source = self.source + text
|
||||
return
|
||||
|
||||
new = io.StringIO()
|
||||
|
||||
# Iterate over the existing document until we hit the edit range,
|
||||
# at which point we write the new text, then loop until we hit
|
||||
# the end of the range and continue writing.
|
||||
for i, line in enumerate(lines):
|
||||
if i < start_line:
|
||||
new.write(line)
|
||||
continue
|
||||
|
||||
if i > end_line:
|
||||
new.write(line)
|
||||
continue
|
||||
|
||||
if i == start_line:
|
||||
new.write(line[:start_col])
|
||||
new.write(text)
|
||||
|
||||
if i == end_line:
|
||||
new.write(line[end_col:])
|
||||
|
||||
self._source = new.getvalue()
|
||||
|
||||
def _apply_full_change(self, change: types.TextDocumentContentChangeEvent) -> None:
|
||||
"""Apply a ``Full`` text change to the document."""
|
||||
self._source = change.text
|
||||
|
||||
def _apply_none_change(self, _: types.TextDocumentContentChangeEvent) -> None:
|
||||
"""Apply a ``None`` text change to the document
|
||||
|
||||
Currently does nothing, provided for consistency.
|
||||
"""
|
||||
pass
|
||||
|
||||
def apply_change(self, change: types.TextDocumentContentChangeEvent) -> None:
|
||||
"""Apply a text change to a document, considering TextDocumentSyncKind
|
||||
|
||||
Performs either
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`,
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Full`, or no synchronization
|
||||
based on both the client request and server capabilities.
|
||||
|
||||
.. admonition:: ``Incremental`` versus ``Full`` synchronization
|
||||
|
||||
Even if a server accepts ``Incremantal`` SyncKinds, clients may request
|
||||
a ``Full`` SyncKind. In LSP 3.x, clients make this request by omitting
|
||||
both Range and RangeLength from their request. Consequently, the
|
||||
attributes "range" and "rangeLength" will be missing from ``Full``
|
||||
content update client requests in the pygls Python library.
|
||||
|
||||
"""
|
||||
if isinstance(change, types.TextDocumentContentChangeEvent_Type1):
|
||||
if self._is_sync_kind_incremental:
|
||||
self._apply_incremental_change(change)
|
||||
return
|
||||
# Log an error, but still perform full update to preserve existing
|
||||
# assumptions in test_document/test_document_full_edit. Test breaks
|
||||
# otherwise, and fixing the tests would require a broader fix to
|
||||
# protocol.py.
|
||||
logger.error(
|
||||
"Unsupported client-provided TextDocumentContentChangeEvent. "
|
||||
"Please update / submit a Pull Request to your LSP client."
|
||||
)
|
||||
|
||||
if self._is_sync_kind_none:
|
||||
self._apply_none_change(change)
|
||||
else:
|
||||
self._apply_full_change(change)
|
||||
|
||||
@property
|
||||
def lines(self) -> List[str]:
|
||||
return self.source.splitlines(True)
|
||||
|
||||
def offset_at_position(self, client_position: types.Position) -> int:
|
||||
"""Return the character offset pointed at by the given client_position."""
|
||||
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]
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
def word_at_position(
|
||||
self,
|
||||
client_position: types.Position,
|
||||
re_start_word: Pattern[str] = RE_START_WORD,
|
||||
re_end_word: Pattern[str] = RE_END_WORD,
|
||||
) -> str:
|
||||
"""Return the word at position.
|
||||
|
||||
The word is constructed in two halves, the first half is found by taking
|
||||
the first match of ``re_start_word`` on the line up until
|
||||
``position.character``.
|
||||
|
||||
The second half is found by taking ``position.character`` up until the
|
||||
last match of ``re_end_word`` on the line.
|
||||
|
||||
:func:`python:re.findall` is used to find the matches.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
The line and character offset.
|
||||
|
||||
re_start_word
|
||||
The regular expression for extracting the word backward from
|
||||
position. The default pattern is ``[A-Za-z_0-9]*$``.
|
||||
|
||||
re_end_word
|
||||
The regular expression for extracting the word forward from
|
||||
position. The default pattern is ``^[A-Za-z_0-9]*``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The word (obtained by concatenating the two matches) at position.
|
||||
"""
|
||||
lines = self.lines
|
||||
if client_position.line >= len(lines):
|
||||
return ""
|
||||
|
||||
server_position = self._position_codec.position_from_client_units(
|
||||
lines, client_position
|
||||
)
|
||||
row, col = server_position.line, server_position.character
|
||||
line = lines[row]
|
||||
# Split word in two
|
||||
start = line[:col]
|
||||
end = line[col:]
|
||||
|
||||
# Take end of start and start of end to find word
|
||||
# These are guaranteed to match, even if they match the empty string
|
||||
m_start = re_start_word.findall(start)
|
||||
m_end = re_end_word.findall(end)
|
||||
|
||||
return m_start[0] + m_end[-1]
|
||||
@@ -0,0 +1,323 @@
|
||||
############################################################################
|
||||
# Original work Copyright 2017 Palantir Technologies, Inc. #
|
||||
# Original work licensed under the MIT License. #
|
||||
# See ThirdPartyNotices.txt in the project root for license information. #
|
||||
# All modifications Copyright (c) Open Law Library. All rights reserved. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from lsprotocol import types
|
||||
from lsprotocol.types import (
|
||||
PositionEncodingKind,
|
||||
TextDocumentSyncKind,
|
||||
WorkspaceFolder,
|
||||
)
|
||||
from pygls.uris import to_fs_path, uri_scheme
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from pygls.workspace.position_codec import PositionCodec
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Workspace(object):
|
||||
def __init__(
|
||||
self,
|
||||
root_uri: Optional[str],
|
||||
sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
workspace_folders: Optional[List[WorkspaceFolder]] = None,
|
||||
position_encoding: Optional[
|
||||
Union[PositionEncodingKind, str]
|
||||
] = PositionEncodingKind.Utf16,
|
||||
):
|
||||
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
|
||||
else:
|
||||
self._root_path = None
|
||||
self._sync_kind = sync_kind
|
||||
self._text_documents: Dict[str, TextDocument] = {}
|
||||
self._notebook_documents: Dict[str, types.NotebookDocument] = {}
|
||||
|
||||
# Used to lookup notebooks which contain a given cell.
|
||||
self._cell_in_notebook: Dict[str, str] = {}
|
||||
self._folders: Dict[str, WorkspaceFolder] = {}
|
||||
self._docs: Dict[str, TextDocument] = {}
|
||||
self._position_encoding = position_encoding
|
||||
self._position_codec = PositionCodec(encoding=position_encoding)
|
||||
|
||||
if workspace_folders is not None:
|
||||
for folder in workspace_folders:
|
||||
self.add_folder(folder)
|
||||
|
||||
@property
|
||||
def position_encoding(self) -> Optional[Union[PositionEncodingKind, str]]:
|
||||
return self._position_encoding
|
||||
|
||||
@property
|
||||
def position_codec(self) -> PositionCodec:
|
||||
return self._position_codec
|
||||
|
||||
def _create_text_document(
|
||||
self,
|
||||
doc_uri: str,
|
||||
source: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
language_id: Optional[str] = None,
|
||||
) -> TextDocument:
|
||||
return TextDocument(
|
||||
doc_uri,
|
||||
source=source,
|
||||
version=version,
|
||||
language_id=language_id,
|
||||
sync_kind=self._sync_kind,
|
||||
position_codec=self._position_codec,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def notebook_documents(self):
|
||||
return self._notebook_documents
|
||||
|
||||
@property
|
||||
def text_documents(self):
|
||||
return self._text_documents
|
||||
|
||||
@property
|
||||
def folders(self):
|
||||
return self._folders
|
||||
|
||||
def get_notebook_document(
|
||||
self, *, notebook_uri: Optional[str] = None, cell_uri: Optional[str] = None
|
||||
) -> Optional[types.NotebookDocument]:
|
||||
"""Return the notebook corresponding with the given uri.
|
||||
|
||||
If both ``notebook_uri`` and ``cell_uri`` are given, ``notebook_uri`` takes
|
||||
precedence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
notebook_uri
|
||||
If given, return the notebook document with the given uri.
|
||||
|
||||
cell_uri
|
||||
If given, return the notebook document which contains a cell with the
|
||||
given uri
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[NotebookDocument]
|
||||
The requested notebook document if found, ``None`` otherwise.
|
||||
"""
|
||||
if notebook_uri is not None:
|
||||
return self._notebook_documents.get(notebook_uri)
|
||||
|
||||
if cell_uri is not None:
|
||||
notebook_uri = self._cell_in_notebook.get(cell_uri)
|
||||
if notebook_uri is None:
|
||||
return None
|
||||
|
||||
return self._notebook_documents.get(notebook_uri)
|
||||
|
||||
return None
|
||||
|
||||
def get_text_document(self, doc_uri: str) -> TextDocument:
|
||||
"""
|
||||
Return a managed document if-present,
|
||||
else create one pointing at disk.
|
||||
|
||||
See https://github.com/Microsoft/language-server-protocol/issues/177
|
||||
"""
|
||||
return self._text_documents.get(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)
|
||||
|
||||
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)
|
||||
|
||||
for cell_document in params.cell_text_documents:
|
||||
self.put_text_document(cell_document, notebook_uri=notebook.uri)
|
||||
|
||||
def put_text_document(
|
||||
self,
|
||||
text_document: types.TextDocumentItem,
|
||||
notebook_uri: Optional[str] = None,
|
||||
):
|
||||
"""Add a text document to the workspace.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text_document
|
||||
The text document to add
|
||||
|
||||
notebook_uri
|
||||
If set, indicates that this text document represents a cell in a notebook
|
||||
document
|
||||
"""
|
||||
doc_uri = text_document.uri
|
||||
|
||||
self._text_documents[doc_uri] = self._create_text_document(
|
||||
doc_uri,
|
||||
source=text_document.text,
|
||||
version=text_document.version,
|
||||
language_id=text_document.language_id,
|
||||
)
|
||||
|
||||
if notebook_uri:
|
||||
self._cell_in_notebook[doc_uri] = notebook_uri
|
||||
|
||||
def remove_notebook_document(self, params: types.DidCloseNotebookDocumentParams):
|
||||
notebook_uri = params.notebook_document.uri
|
||||
self._notebook_documents.pop(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)
|
||||
|
||||
def remove_folder(self, folder_uri: str):
|
||||
self._folders.pop(folder_uri, None)
|
||||
try:
|
||||
del self._folders[folder_uri]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def root_path(self):
|
||||
return self._root_path
|
||||
|
||||
@property
|
||||
def root_uri(self):
|
||||
return self._root_uri
|
||||
|
||||
def update_notebook_document(self, params: types.DidChangeNotebookDocumentParams):
|
||||
uri = params.notebook_document.uri
|
||||
notebook = self._notebook_documents[uri]
|
||||
notebook.version = params.notebook_document.version
|
||||
|
||||
if params.change.metadata:
|
||||
notebook.metadata = params.change.metadata
|
||||
|
||||
cell_changes = params.change.cells
|
||||
if cell_changes is None:
|
||||
return
|
||||
|
||||
# Process changes to any cell metadata.
|
||||
nb_cells = {cell.document: cell for cell in notebook.cells}
|
||||
for new_data in cell_changes.data or []:
|
||||
nb_cell = nb_cells.get(new_data.document)
|
||||
if nb_cell is None:
|
||||
logger.warning(
|
||||
"Ignoring metadata for '%s': not in notebook.", new_data.document
|
||||
)
|
||||
continue
|
||||
|
||||
nb_cell.kind = new_data.kind
|
||||
nb_cell.metadata = new_data.metadata
|
||||
nb_cell.execution_summary = new_data.execution_summary
|
||||
|
||||
# Process changes to the notebook's structure
|
||||
structure = cell_changes.structure
|
||||
if structure:
|
||||
cells = notebook.cells
|
||||
new_cells = structure.array.cells or []
|
||||
|
||||
# Re-order the cells
|
||||
before = cells[: structure.array.start]
|
||||
after = cells[(structure.array.start + structure.array.delete_count) :]
|
||||
notebook.cells = [*before, *new_cells, *after]
|
||||
|
||||
for new_cell in structure.did_open or []:
|
||||
self.put_text_document(new_cell, notebook_uri=uri)
|
||||
|
||||
for removed_cell in structure.did_close or []:
|
||||
self.remove_text_document(removed_cell.uri)
|
||||
|
||||
# Process changes to the text content of existing cells.
|
||||
for text in cell_changes.text_content or []:
|
||||
for change in text.changes:
|
||||
self.update_text_document(text.document, change)
|
||||
|
||||
def update_text_document(
|
||||
self,
|
||||
text_doc: types.VersionedTextDocumentIdentifier,
|
||||
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)
|
||||
Reference in New Issue
Block a user