617 lines
20 KiB
Python
617 lines
20 KiB
Python
############################################################################
|
|
# 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
|