From 2fae8e35a54ef0173a7f13cd7016a724587d9958 Mon Sep 17 00:00:00 2001 From: Daniel Biehl Date: Sat, 30 Dec 2023 15:01:03 +0100 Subject: [PATCH] refactor(langserver): implement cancelable threads and remove async code from definition, implementations, hover, folding --- .../src/robotcode/core/utils/threading.py | 68 ++++++++++- .../src/robotcode/jsonrpc2/protocol.py | 114 ++++++++++-------- .../common/parts/definition.py | 16 +-- .../common/parts/folding_range.py | 30 ++--- .../language_server/common/parts/hover.py | 8 +- .../common/parts/implementation.py | 16 +-- .../common/parts/inlay_hint.py | 4 +- .../language_server/common/text_document.py | 23 +--- .../robotframework/parts/codelens.py | 3 +- .../robotframework/parts/folding_range.py | 4 +- .../robotframework/parts/goto.py | 17 ++- .../robotframework/parts/hover.py | 9 ++ .../common/test_text_document.py | 67 ++++------ .../parts/data/.vscode/settings.json | 12 +- .../robotframework/parts/test_foldingrange.py | 9 +- .../parts/test_goto_definition.py | 14 +-- .../parts/test_goto_implementation.py | 17 +-- .../robotframework/parts/test_hover.py | 4 +- 18 files changed, 240 insertions(+), 195 deletions(-) diff --git a/packages/core/src/robotcode/core/utils/threading.py b/packages/core/src/robotcode/core/utils/threading.py index a9d699b94..41a9e7ddd 100644 --- a/packages/core/src/robotcode/core/utils/threading.py +++ b/packages/core/src/robotcode/core/utils/threading.py @@ -1,11 +1,27 @@ import inspect -from typing import Any, Callable, TypeVar +from concurrent.futures import CancelledError, Future +from threading import Thread, current_thread, local +from typing import Any, Callable, Dict, Generic, Optional, Tuple, TypeVar, cast _F = TypeVar("_F", bound=Callable[..., Any]) +_TResult = TypeVar("_TResult") __THREADED_MARKER = "__threaded__" +class FutureEx(Future, Generic[_TResult]): # type: ignore[type-arg] + def __init__(self) -> None: + super().__init__() + self.cancelation_requested = False + + def cancel(self) -> bool: + self.cancelation_requested = True + return super().cancel() + + def result(self, timeout: Optional[float] = None) -> _TResult: + return cast(_TResult, super().result(timeout)) + + def threaded(enabled: bool = True) -> Callable[[_F], _F]: def decorator(func: _F) -> _F: setattr(func, __THREADED_MARKER, enabled) @@ -14,5 +30,51 @@ def decorator(func: _F) -> _F: return decorator -def is_threaded_callable(func: Callable[..., Any]) -> bool: - return getattr(func, __THREADED_MARKER, False) or inspect.ismethod(func) and getattr(func, __THREADED_MARKER, False) +def is_threaded_callable(callable: Callable[..., Any]) -> bool: + return ( + getattr(callable, __THREADED_MARKER, False) + or inspect.ismethod(callable) + and getattr(callable, __THREADED_MARKER, False) + ) + + +class _Local(local): + def __init__(self) -> None: + super().__init__() + self._local_future: Optional[FutureEx[Any]] = None + + +_local_storage = _Local() + + +def _run_callable_in_thread_handler( + future: FutureEx[_TResult], callable: Callable[..., _TResult], args: Tuple[Any, ...], kwargs: Dict[str, Any] +) -> None: + _local_storage._local_future = future + future.set_running_or_notify_cancel() + try: + future.set_result(callable(*args, **kwargs)) + except Exception as e: + # TODO: add traceback to exception e.traceback = format_exc() + + future.set_exception(e) + finally: + _local_storage._local_future = None + + +def is_thread_cancelled() -> bool: + return _local_storage._local_future is not None and _local_storage._local_future.cancelation_requested + + +def check_thread_canceled() -> None: + if _local_storage._local_future is not None and _local_storage._local_future.cancelation_requested: + name = current_thread().name + raise CancelledError(f"Thread {name+' ' if name else ' '}Cancelled") + + +def run_callable_in_thread(callable: Callable[..., _TResult], *args: Any, **kwargs: Any) -> FutureEx[_TResult]: + future: FutureEx[_TResult] = FutureEx() + + Thread(target=_run_callable_in_thread_handler, args=(future, callable, args, kwargs), name=str(callable)).start() + + return future diff --git a/packages/jsonrpc2/src/robotcode/jsonrpc2/protocol.py b/packages/jsonrpc2/src/robotcode/jsonrpc2/protocol.py index 57872657c..af5d2a569 100644 --- a/packages/jsonrpc2/src/robotcode/jsonrpc2/protocol.py +++ b/packages/jsonrpc2/src/robotcode/jsonrpc2/protocol.py @@ -2,11 +2,11 @@ import asyncio import concurrent.futures +import functools import inspect import json import re import threading -import time import weakref from abc import ABC, abstractmethod from collections import OrderedDict @@ -21,7 +21,6 @@ Iterator, List, Mapping, - NamedTuple, Optional, Protocol, Set, @@ -42,7 +41,7 @@ from robotcode.core.utils.dataclasses import as_json, from_dict from robotcode.core.utils.inspect import ensure_coroutine, iter_methods from robotcode.core.utils.logging import LoggingDescriptor -from robotcode.core.utils.threading import is_threaded_callable +from robotcode.core.utils.threading import is_threaded_callable, run_callable_in_thread __all__ = [ "JsonRPCErrors", @@ -344,15 +343,23 @@ def get_param_type(self, name: str) -> Optional[Type[Any]]: return result.param_type -class SendedRequestEntry(NamedTuple): - future: concurrent.futures.Future[Any] - result_type: Optional[Type[Any]] +class SendedRequestEntry: + def __init__(self, future: concurrent.futures.Future[Any], result_type: Optional[Type[Any]]) -> None: + self.future = future + self.result_type = result_type -class ReceivedRequestEntry(NamedTuple): - future: asyncio.Future[Any] - request: Optional[Any] - cancelable: bool +class ReceivedRequestEntry: + def __init__(self, future: asyncio.Future[Any], request: JsonRPCRequest, cancelable: bool) -> None: + self.future = future + self.request = request + self.cancelable = cancelable + self.cancel_requested = False + + def cancel(self) -> None: + self.cancel_requested = True + if self.future is not None and not self.future.cancelled(): + self.future.cancel() class JsonRPCProtocolBase(asyncio.Protocol, ABC): @@ -711,7 +718,6 @@ def _convert_params( return args, kw_args async def handle_request(self, message: JsonRPCRequest) -> None: - start = time.monotonic_ns() try: e = self.registry.get_entry(message.method) @@ -725,13 +731,18 @@ async def handle_request(self, message: JsonRPCRequest) -> None: params = self._convert_params(e.method, e.param_type, message.params) - if not e.is_coroutine: + is_threaded_method = is_threaded_callable(e.method) + + if not is_threaded_method and not e.is_coroutine: self.send_response(message.id, e.method(*params[0], **params[1])) else: - if is_threaded_callable(e.method): - task = run_coroutine_in_thread( - ensure_coroutine(cast(Callable[..., Any], e.method)), *params[0], **params[1] - ) + if is_threaded_method: + if e.is_coroutine: + task = run_coroutine_in_thread( + ensure_coroutine(cast(Callable[..., Any], e.method)), *params[0], **params[1] + ) + else: + task = asyncio.wrap_future(run_callable_in_thread(e.method, *params[0], **params[1])) else: task = create_sub_task( ensure_coroutine(e.method)(*params[0], **params[1]), @@ -741,49 +752,56 @@ async def handle_request(self, message: JsonRPCRequest) -> None: with self._received_request_lock: self._received_request[message.id] = ReceivedRequestEntry(task, message, e.cancelable) - def done(t: asyncio.Future[Any]) -> None: - try: - if not t.cancelled(): - ex = t.exception() - if ex is not None: - self.__logger.exception(ex, exc_info=ex) - raise JsonRPCErrorException( - JsonRPCErrors.INTERNAL_ERROR, f"{type(ex).__name__}: {ex}" - ) from ex - - self.send_response(message.id, t.result()) - except asyncio.CancelledError: - self.__logger.debug(lambda: f"request message {message!r} canceled") - self.send_error(JsonRPCErrors.REQUEST_CANCELLED, "Request canceled.", id=message.id) - except (SystemExit, KeyboardInterrupt): - raise - except JsonRPCErrorException as e: - self.send_error(e.code, e.message or f"{type(e).__name__}: {e}", id=message.id, data=e.data) - except BaseException as e: - self.__logger.exception(e) - self.send_error(JsonRPCErrors.INTERNAL_ERROR, f"{type(e).__name__}: {e}", id=message.id) - finally: - with self._received_request_lock: - self._received_request.pop(message.id, None) - - task.add_done_callback(done) + task.add_done_callback(functools.partial(self._received_request_done, message)) await task - finally: - self.__logger.debug(lambda: f"request message {message!r} done in {time.monotonic_ns() - start}ns") + except (SystemExit, KeyboardInterrupt, asyncio.CancelledError): + raise + except BaseException as e: + self.__logger.exception(e) + + def _received_request_done(self, message: JsonRPCRequest, t: asyncio.Future[Any]) -> None: + try: + with self._received_request_lock: + entry = self._received_request.pop(message.id, None) + + if entry is None: + self.__logger.critical(lambda: f"unknown request {message!r}") + return + + if entry.cancel_requested: + self.__logger.debug(lambda: f"request {message!r} canceled") + self.send_error(JsonRPCErrors.REQUEST_CANCELLED, "Request canceled.", id=message.id) + else: + if not t.cancelled(): + ex = t.exception() + if ex is not None: + self.__logger.exception(ex, exc_info=ex) + raise JsonRPCErrorException(JsonRPCErrors.INTERNAL_ERROR, f"{type(ex).__name__}: {ex}") from ex + + self.send_response(message.id, t.result()) + except asyncio.CancelledError: + self.__logger.debug(lambda: f"request message {message!r} canceled") + self.send_error(JsonRPCErrors.REQUEST_CANCELLED, "Request canceled.", id=message.id) + except (SystemExit, KeyboardInterrupt): + raise + except JsonRPCErrorException as e: + self.send_error(e.code, e.message or f"{type(e).__name__}: {e}", id=message.id, data=e.data) + except BaseException as e: + self.__logger.exception(e) + self.send_error(JsonRPCErrors.INTERNAL_ERROR, f"{type(e).__name__}: {e}", id=message.id) def cancel_request(self, id: Union[int, str, None]) -> None: with self._received_request_lock: entry = self._received_request.get(id, None) - if entry is not None and entry.future is not None and not entry.future.cancelled(): + if entry is not None: self.__logger.debug(lambda: f"try to cancel request {entry.request if entry is not None else ''}") - entry.future.cancel() + entry.cancel() def cancel_all_received_request(self) -> None: for entry in self._received_request.values(): - if entry is not None and entry.cancelable and entry.future is not None and not entry.future.cancelled(): - entry.future.cancel() + entry.cancel() @__logger.call async def handle_notification(self, message: JsonRPCNotification) -> None: diff --git a/packages/language_server/src/robotcode/language_server/common/parts/definition.py b/packages/language_server/src/robotcode/language_server/common/parts/definition.py index de7aa32c9..150528847 100644 --- a/packages/language_server/src/robotcode/language_server/common/parts/definition.py +++ b/packages/language_server/src/robotcode/language_server/common/parts/definition.py @@ -1,9 +1,9 @@ from __future__ import annotations -from asyncio import CancelledError +from concurrent.futures import CancelledError from typing import TYPE_CHECKING, Any, Final, List, Optional, Union -from robotcode.core.async_tools import async_tasking_event +from robotcode.core.event import event from robotcode.core.lsp.types import ( DefinitionParams, Location, @@ -13,7 +13,7 @@ TextDocumentIdentifier, ) from robotcode.core.utils.logging import LoggingDescriptor -from robotcode.core.utils.threading import threaded +from robotcode.core.utils.threading import check_thread_canceled, threaded from robotcode.jsonrpc2.protocol import rpc_method from robotcode.language_server.common.decorators import language_id_filter from robotcode.language_server.common.has_extend_capabilities import HasExtendCapabilities @@ -31,8 +31,8 @@ def __init__(self, parent: LanguageServerProtocol) -> None: super().__init__(parent) self.link_support = False - @async_tasking_event - async def collect( + @event + def collect( sender, document: TextDocument, position: Position # NOSONAR ) -> Union[Location, List[Location], List[LocationLink], None]: ... @@ -50,7 +50,7 @@ def extend_capabilities(self, capabilities: ServerCapabilities) -> None: @rpc_method(name="textDocument/definition", param_type=DefinitionParams) @threaded() - async def _text_document_definition( + def _text_document_definition( self, text_document: TextDocumentIdentifier, position: Position, *args: Any, **kwargs: Any ) -> Optional[Union[Location, List[Location], List[LocationLink]]]: locations: List[Location] = [] @@ -60,9 +60,11 @@ async def _text_document_definition( if document is None: return None - for result in await self.collect( + for result in self.collect( self, document, document.position_from_utf16(position), callback_filter=language_id_filter(document) ): + check_thread_canceled() + if isinstance(result, BaseException): if not isinstance(result, CancelledError): self._logger.exception(result, exc_info=result) diff --git a/packages/language_server/src/robotcode/language_server/common/parts/folding_range.py b/packages/language_server/src/robotcode/language_server/common/parts/folding_range.py index bf2f65ae6..5f876ca76 100644 --- a/packages/language_server/src/robotcode/language_server/common/parts/folding_range.py +++ b/packages/language_server/src/robotcode/language_server/common/parts/folding_range.py @@ -1,9 +1,7 @@ -from __future__ import annotations - -from asyncio import CancelledError +from concurrent.futures import CancelledError from typing import TYPE_CHECKING, Any, Final, List, Optional -from robotcode.core.async_tools import async_tasking_event +from robotcode.core.event import event from robotcode.core.lsp.types import ( FoldingRange, FoldingRangeParams, @@ -26,11 +24,11 @@ class FoldingRangeProtocolPart(LanguageServerProtocolPart, HasExtendCapabilities): _logger: Final = LoggingDescriptor() - def __init__(self, parent: LanguageServerProtocol) -> None: + def __init__(self, parent: "LanguageServerProtocol") -> None: super().__init__(parent) - @async_tasking_event - async def collect(sender, document: TextDocument) -> Optional[List[FoldingRange]]: # pragma: no cover, NOSONAR + @event + def collect(sender, document: TextDocument) -> Optional[List[FoldingRange]]: # pragma: no cover, NOSONAR ... def extend_capabilities(self, capabilities: ServerCapabilities) -> None: @@ -39,7 +37,7 @@ def extend_capabilities(self, capabilities: ServerCapabilities) -> None: @rpc_method(name="textDocument/foldingRange", param_type=FoldingRangeParams) @threaded() - async def _text_document_folding_range( + def _text_document_folding_range( self, text_document: TextDocumentIdentifier, *args: Any, **kwargs: Any ) -> Optional[List[FoldingRange]]: results: List[FoldingRange] = [] @@ -47,7 +45,7 @@ async def _text_document_folding_range( if document is None: return None - for result in await self.collect(self, document, callback_filter=language_id_filter(document)): + for result in self.collect(self, document, callback_filter=language_id_filter(document)): if isinstance(result, BaseException): if not isinstance(result, CancelledError): self._logger.exception(result, exc_info=result) @@ -58,14 +56,10 @@ async def _text_document_folding_range( if not results: return None - for result in results: - if result.start_character is not None: - result.start_character = document.position_to_utf16( - Position(result.start_line, result.start_character) - ).character - if result.end_character is not None: - result.end_character = document.position_to_utf16( - Position(result.end_line, result.end_character) - ).character + for r in results: + if r.start_character is not None: + r.start_character = document.position_to_utf16(Position(r.start_line, r.start_character)).character + if r.end_character is not None: + r.end_character = document.position_to_utf16(Position(r.end_line, r.end_character)).character return results diff --git a/packages/language_server/src/robotcode/language_server/common/parts/hover.py b/packages/language_server/src/robotcode/language_server/common/parts/hover.py index b843706a9..348ccf1a3 100644 --- a/packages/language_server/src/robotcode/language_server/common/parts/hover.py +++ b/packages/language_server/src/robotcode/language_server/common/parts/hover.py @@ -1,3 +1,4 @@ +from concurrent.futures import CancelledError from typing import TYPE_CHECKING, Any, Final, List, Optional from robotcode.core.event import event @@ -10,7 +11,7 @@ TextDocumentIdentifier, ) from robotcode.core.utils.logging import LoggingDescriptor -from robotcode.core.utils.threading import threaded +from robotcode.core.utils.threading import check_thread_canceled, threaded from robotcode.jsonrpc2.protocol import rpc_method from robotcode.language_server.common.decorators import language_id_filter from robotcode.language_server.common.has_extend_capabilities import ( @@ -60,8 +61,11 @@ def _text_document_hover( document.position_from_utf16(position), callback_filter=language_id_filter(document), ): + check_thread_canceled() + if isinstance(result, BaseException): - self._logger.exception(result, exc_info=result) + if not isinstance(result, CancelledError): + self._logger.exception(result, exc_info=result) else: if result is not None: results.append(result) diff --git a/packages/language_server/src/robotcode/language_server/common/parts/implementation.py b/packages/language_server/src/robotcode/language_server/common/parts/implementation.py index e4d87a450..9944f3df3 100644 --- a/packages/language_server/src/robotcode/language_server/common/parts/implementation.py +++ b/packages/language_server/src/robotcode/language_server/common/parts/implementation.py @@ -1,9 +1,9 @@ from __future__ import annotations -from asyncio import CancelledError +from concurrent.futures import CancelledError from typing import TYPE_CHECKING, Any, Final, List, Optional, Union -from robotcode.core.async_tools import async_tasking_event +from robotcode.core.event import event from robotcode.core.lsp.types import ( ImplementationParams, Location, @@ -13,7 +13,7 @@ TextDocumentIdentifier, ) from robotcode.core.utils.logging import LoggingDescriptor -from robotcode.core.utils.threading import threaded +from robotcode.core.utils.threading import check_thread_canceled, threaded from robotcode.jsonrpc2.protocol import rpc_method from robotcode.language_server.common.decorators import language_id_filter from robotcode.language_server.common.has_extend_capabilities import HasExtendCapabilities @@ -32,8 +32,8 @@ def __init__(self, parent: LanguageServerProtocol) -> None: super().__init__(parent) self.link_support = False - @async_tasking_event - async def collect( + @event + def collect( sender, document: TextDocument, position: Position # NOSONAR ) -> Union[Location, List[Location], List[LocationLink], None]: ... @@ -51,7 +51,7 @@ def extend_capabilities(self, capabilities: ServerCapabilities) -> None: @rpc_method(name="textDocument/implementation", param_type=ImplementationParams) @threaded() - async def _text_document_implementation( + def _text_document_implementation( self, text_document: TextDocumentIdentifier, position: Position, *args: Any, **kwargs: Any ) -> Optional[Union[Location, List[Location], List[LocationLink]]]: locations: List[Location] = [] @@ -61,12 +61,14 @@ async def _text_document_implementation( if document is None: return None - for result in await self.collect( + for result in self.collect( self, document, position, callback_filter=language_id_filter(document), ): + check_thread_canceled() + if isinstance(result, BaseException): if not isinstance(result, CancelledError): self._logger.exception(result, exc_info=result) diff --git a/packages/language_server/src/robotcode/language_server/common/parts/inlay_hint.py b/packages/language_server/src/robotcode/language_server/common/parts/inlay_hint.py index 175ded5e1..146d7dc1e 100644 --- a/packages/language_server/src/robotcode/language_server/common/parts/inlay_hint.py +++ b/packages/language_server/src/robotcode/language_server/common/parts/inlay_hint.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from asyncio import CancelledError from typing import TYPE_CHECKING, Any, Final, List, Optional @@ -28,7 +26,7 @@ class InlayHintProtocolPart(LanguageServerProtocolPart, HasExtendCapabilities): _logger: Final = LoggingDescriptor() - def __init__(self, parent: LanguageServerProtocol) -> None: + def __init__(self, parent: "LanguageServerProtocol") -> None: super().__init__(parent) @async_tasking_event diff --git a/packages/language_server/src/robotcode/language_server/common/text_document.py b/packages/language_server/src/robotcode/language_server/common/text_document.py index 1d135e63f..e71e3fba4 100644 --- a/packages/language_server/src/robotcode/language_server/common/text_document.py +++ b/packages/language_server/src/robotcode/language_server/common/text_document.py @@ -5,7 +5,7 @@ import io import threading import weakref -from typing import Any, Awaitable, Callable, Dict, Final, List, Optional, TypeVar, Union, cast +from typing import Any, Callable, Dict, Final, List, Optional, TypeVar, Union, cast from robotcode.core.event import event from robotcode.core.lsp.types import DocumentUri, Position, Range @@ -255,26 +255,7 @@ def get_cache( return cast(_T, e.data) - async def get_cache_async( - self, - entry: Union[Callable[[TextDocument], Awaitable[_T]], Callable[..., Awaitable[_T]]], - *args: Any, - **kwargs: Any, - ) -> _T: - reference = self.__get_cache_reference(entry) - - e = self._cache[reference] - - with e.lock: - if not e.has_data: - e.data = await entry(self, *args, **kwargs) - e.has_data = True - - return cast(_T, e.data) - - async def remove_cache_entry( - self, entry: Union[Callable[[TextDocument], Awaitable[_T]], Callable[..., Awaitable[_T]]] - ) -> None: + def remove_cache_entry(self, entry: Union[Callable[[TextDocument], _T], Callable[..., _T]]) -> None: self.__remove_cache_entry(self.__get_cache_reference(entry, add_remove=False)) def set_data(self, key: Any, data: Any) -> None: diff --git a/packages/language_server/src/robotcode/language_server/robotframework/parts/codelens.py b/packages/language_server/src/robotcode/language_server/robotframework/parts/codelens.py index 7f5a20750..f4c018a89 100644 --- a/packages/language_server/src/robotcode/language_server/robotframework/parts/codelens.py +++ b/packages/language_server/src/robotcode/language_server/robotframework/parts/codelens.py @@ -141,7 +141,8 @@ async def find_refs() -> None: task = create_sub_task(find_refs(), loop=self.parent.diagnostics.diagnostics_loop) def done(task: Any) -> None: - self._running_task.remove(key) + if key in self._running_task: + self._running_task.remove(key) task.add_done_callback(done) diff --git a/packages/language_server/src/robotcode/language_server/robotframework/parts/folding_range.py b/packages/language_server/src/robotcode/language_server/robotframework/parts/folding_range.py index 1e79ddd81..c195c13d5 100644 --- a/packages/language_server/src/robotcode/language_server/robotframework/parts/folding_range.py +++ b/packages/language_server/src/robotcode/language_server/robotframework/parts/folding_range.py @@ -6,6 +6,7 @@ from robot.parsing.model.blocks import If, Keyword, TestCase from robotcode.core.lsp.types import FoldingRange from robotcode.core.utils.logging import LoggingDescriptor +from robotcode.core.utils.threading import check_thread_canceled from robotcode.robot.utils.visitor import Visitor from ...common.decorators import language_id @@ -37,6 +38,7 @@ def __init__(self, parent: RobotFoldingRangeProtocolPart) -> None: self.current_if: List[ast.AST] = [] def visit(self, node: ast.AST) -> None: + check_thread_canceled() super().visit(node) @classmethod @@ -123,5 +125,5 @@ def __init__(self, parent: RobotLanguageServerProtocol) -> None: @language_id("robotframework") @_logger.call - async def collect(self, sender: Any, document: TextDocument) -> Optional[List[FoldingRange]]: + def collect(self, sender: Any, document: TextDocument) -> Optional[List[FoldingRange]]: return _Visitor.find_from(self.parent.documents_cache.get_model(document, False), self) diff --git a/packages/language_server/src/robotcode/language_server/robotframework/parts/goto.py b/packages/language_server/src/robotcode/language_server/robotframework/parts/goto.py index 7e48a3566..5a84c2521 100644 --- a/packages/language_server/src/robotcode/language_server/robotframework/parts/goto.py +++ b/packages/language_server/src/robotcode/language_server/robotframework/parts/goto.py @@ -12,6 +12,7 @@ from robotcode.core.lsp.types import Location, LocationLink, Position, Range from robotcode.core.uri import Uri from robotcode.core.utils.logging import LoggingDescriptor +from robotcode.core.utils.threading import check_thread_canceled from robotcode.robot.utils.ast import range_from_token from ...common.decorators import language_id @@ -33,19 +34,19 @@ def __init__(self, parent: RobotLanguageServerProtocol) -> None: @language_id("robotframework") @_logger.call - async def collect_definition( + def collect_definition( self, sender: Any, document: TextDocument, position: Position ) -> Union[Location, List[Location], List[LocationLink], None]: - return await self.collect(document, position) + return self.collect(document, position) @language_id("robotframework") @_logger.call - async def collect_implementation( + def collect_implementation( self, sender: Any, document: TextDocument, position: Position ) -> Union[Location, List[Location], List[LocationLink], None]: - return await self.collect(document, position) + return self.collect(document, position) - async def collect( + def collect( self, document: TextDocument, position: Position ) -> Union[Location, List[Location], List[LocationLink], None]: namespace = self.parent.documents_cache.get_namespace(document) @@ -56,6 +57,8 @@ async def collect( result = [] for variable, var_refs in all_variable_refs.items(): + check_thread_canceled() + found_range = ( variable.name_range if variable.source == namespace.source and position.is_in_range(variable.name_range, False) @@ -82,6 +85,8 @@ async def collect( result = [] for kw, kw_refs in all_kw_refs.items(): + check_thread_canceled() + found_range = ( kw.name_range if kw.source == namespace.source and position.is_in_range(kw.name_range, False) @@ -105,6 +110,8 @@ async def collect( all_namespace_refs = namespace.get_namespace_references() if all_namespace_refs: + check_thread_canceled() + result = [] for ns, ns_refs in all_namespace_refs.items(): diff --git a/packages/language_server/src/robotcode/language_server/robotframework/parts/hover.py b/packages/language_server/src/robotcode/language_server/robotframework/parts/hover.py index 8d2510b97..3cea37470 100644 --- a/packages/language_server/src/robotcode/language_server/robotframework/parts/hover.py +++ b/packages/language_server/src/robotcode/language_server/robotframework/parts/hover.py @@ -17,6 +17,7 @@ from robot.parsing.model.statements import Documentation, Tags from robotcode.core.lsp.types import Hover, MarkupContent, MarkupKind, Position, Range from robotcode.core.utils.logging import LoggingDescriptor +from robotcode.core.utils.threading import check_thread_canceled from robotcode.robot.utils.ast import get_nodes_at_position, range_from_node, range_from_token from robotcode.robot.utils.markdownformatter import MarkDownFormatter @@ -69,6 +70,8 @@ def collect(self, sender: Any, document: TextDocument, position: Position) -> Op return result for result_node in reversed(result_nodes): + check_thread_canceled() + method = self._find_method(type(result_node)) if method is not None: result = method(result_node, result_nodes, document, position) @@ -86,6 +89,8 @@ def _hover_default(self, nodes: List[ast.AST], document: TextDocument, position: highlight_range = None for variable, var_refs in all_variable_refs.items(): + check_thread_canceled() + found_range = ( variable.name_range if variable.source == namespace.source and position.is_in_range(variable.name_range, False) @@ -131,6 +136,8 @@ def _hover_default(self, nodes: List[ast.AST], document: TextDocument, position: result: List[Tuple[Range, str]] = [] for kw, kw_refs in all_kw_refs.items(): + check_thread_canceled() + found_range = ( kw.name_range if kw.source == namespace.source and position.is_in_range(kw.name_range, False) @@ -153,6 +160,8 @@ def _hover_default(self, nodes: List[ast.AST], document: TextDocument, position: all_namespace_refs = namespace.get_namespace_references() if all_namespace_refs: for ns, ns_refs in all_namespace_refs.items(): + check_thread_canceled() + found_range = ( ns.import_range if ns.import_source == namespace.source and position.is_in_range(ns.import_range, False) diff --git a/tests/robotcode/language_server/common/test_text_document.py b/tests/robotcode/language_server/common/test_text_document.py index a7bd34353..3cac784c4 100644 --- a/tests/robotcode/language_server/common/test_text_document.py +++ b/tests/robotcode/language_server/common/test_text_document.py @@ -1,5 +1,4 @@ import pytest -from robotcode.core.async_tools import check_canceled from robotcode.core.lsp.types import Position, Range from robotcode.language_server.common.text_document import ( InvalidRangeError, @@ -7,8 +6,7 @@ ) -@pytest.mark.asyncio() -async def test_apply_full_change_should_work() -> None: +def test_apply_full_change_should_work() -> None: text = """first""" new_text = """changed""" document = TextDocument(document_uri="file:///test.robot", language_id="robotframework", version=1, text=text) @@ -19,8 +17,7 @@ async def test_apply_full_change_should_work() -> None: assert document.text() == new_text -@pytest.mark.asyncio() -async def test_apply_apply_incremental_change_at_begining_should_work() -> None: +def test_apply_apply_incremental_change_at_begining_should_work() -> None: text = """first""" new_text = """changed""" document = TextDocument(document_uri="file:///test.robot", language_id="robotframework", version=1, text=text) @@ -33,8 +30,7 @@ async def test_apply_apply_incremental_change_at_begining_should_work() -> None: assert document.text() == new_text + text -@pytest.mark.asyncio() -async def test_apply_apply_incremental_change_at_end_should_work() -> None: +def test_apply_apply_incremental_change_at_end_should_work() -> None: text = """first""" new_text = """changed""" @@ -48,8 +44,7 @@ async def test_apply_apply_incremental_change_at_end_should_work() -> None: assert document.text() == text + new_text -@pytest.mark.asyncio() -async def test_save_and_revert_should_work() -> None: +def test_save_and_revert_should_work() -> None: text = """first""" new_text = """changed""" @@ -81,8 +76,7 @@ async def test_save_and_revert_should_work() -> None: assert document.version == 2 -@pytest.mark.asyncio() -async def test_apply_apply_incremental_change_in_the_middle_should_work() -> None: +def test_apply_apply_incremental_change_in_the_middle_should_work() -> None: text = """\ first line second line @@ -103,8 +97,7 @@ async def test_apply_apply_incremental_change_in_the_middle_should_work() -> Non assert document.text() == expected -@pytest.mark.asyncio() -async def test_apply_apply_incremental_change_with_start_line_eq_len_lines_should_work() -> None: +def test_apply_apply_incremental_change_with_start_line_eq_len_lines_should_work() -> None: text = """\ first line second line @@ -121,8 +114,7 @@ async def test_apply_apply_incremental_change_with_start_line_eq_len_lines_shoul assert document.text() == text + new_text -@pytest.mark.asyncio() -async def test_apply_apply_incremental_change_with_wrong_range_should_raise_invalidrangerrror() -> None: +def test_apply_apply_incremental_change_with_wrong_range_should_raise_invalidrangerrror() -> None: text = """first""" new_text = """changed""" @@ -135,8 +127,7 @@ async def test_apply_apply_incremental_change_with_wrong_range_should_raise_inva ) -@pytest.mark.asyncio() -async def test_apply_none_change_should_work() -> None: +def test_apply_none_change_should_work() -> None: text = """first""" document = TextDocument(document_uri="file:///test.robot", language_id="robotframework", version=1, text=text) @@ -147,8 +138,7 @@ async def test_apply_none_change_should_work() -> None: assert document.text() == text -@pytest.mark.asyncio() -async def test_lines_should_give_the_lines_of_the_document() -> None: +def test_lines_should_give_the_lines_of_the_document() -> None: text = """\ first second @@ -163,8 +153,7 @@ async def test_lines_should_give_the_lines_of_the_document() -> None: assert document.get_lines() == text.splitlines(True) -@pytest.mark.asyncio() -async def test_document_get_set_clear_data_should_work() -> None: +def test_document_get_set_clear_data_should_work() -> None: text = """\ first second @@ -190,8 +179,7 @@ class WeakReferencable: assert document.get_data(key, None) is None -@pytest.mark.asyncio() -async def test_document_get_set_cache_with_function_should_work() -> None: +def test_document_get_set_cache_with_function_should_work() -> None: text = """\ first second @@ -199,30 +187,29 @@ async def test_document_get_set_cache_with_function_should_work() -> None: """ prefix = "1" - async def get_data(document: TextDocument, data: str) -> str: + def get_data(document: TextDocument, data: str) -> str: return prefix + data document = TextDocument(document_uri="file:///test.robot", language_id="robotframework", version=1, text=text) - assert await document.get_cache_async(get_data, "data") == "1data" + assert document.get_cache(get_data, "data") == "1data" prefix = "2" - assert await document.get_cache_async(get_data, "data1") == "1data" + assert document.get_cache(get_data, "data1") == "1data" - await document.remove_cache_entry(get_data) + document.remove_cache_entry(get_data) - assert await document.get_cache_async(get_data, "data2") == "2data2" + assert document.get_cache(get_data, "data2") == "2data2" prefix = "3" - assert await document.get_cache_async(get_data, "data3") == "2data2" + assert document.get_cache(get_data, "data3") == "2data2" document.invalidate_cache() - assert await document.get_cache_async(get_data, "data3") == "3data3" + assert document.get_cache(get_data, "data3") == "3data3" -@pytest.mark.asyncio() -async def test_document_get_set_cache_with_method_should_work() -> None: +def test_document_get_set_cache_with_method_should_work() -> None: text = """\ first second @@ -233,29 +220,27 @@ async def test_document_get_set_cache_with_method_should_work() -> None: prefix = "1" class Dummy: - async def get_data(self, document: TextDocument, data: str) -> str: + def get_data(self, document: TextDocument, data: str) -> str: return prefix + data dummy = Dummy() - assert await document.get_cache_async(dummy.get_data, "data") == "1data" + assert document.get_cache(dummy.get_data, "data") == "1data" prefix = "2" - assert await document.get_cache_async(dummy.get_data, "data1") == "1data" + assert document.get_cache(dummy.get_data, "data1") == "1data" - await document.remove_cache_entry(dummy.get_data) + document.remove_cache_entry(dummy.get_data) - assert await document.get_cache_async(dummy.get_data, "data2") == "2data2" + assert document.get_cache(dummy.get_data, "data2") == "2data2" prefix = "3" - assert await document.get_cache_async(dummy.get_data, "data3") == "2data2" + assert document.get_cache(dummy.get_data, "data3") == "2data2" document.invalidate_cache() - assert await document.get_cache_async(dummy.get_data, "data3") == "3data3" + assert document.get_cache(dummy.get_data, "data3") == "3data3" del dummy - await check_canceled() - assert len(document._cache) == 0 diff --git a/tests/robotcode/language_server/robotframework/parts/data/.vscode/settings.json b/tests/robotcode/language_server/robotframework/parts/data/.vscode/settings.json index 8cd53caea..8cc0dac14 100644 --- a/tests/robotcode/language_server/robotframework/parts/data/.vscode/settings.json +++ b/tests/robotcode/language_server/robotframework/parts/data/.vscode/settings.json @@ -34,12 +34,12 @@ "**/lib/alibrary.py", "LibraryWithErrors" ], - // "robotcode.extraArgs": [ - // "--log", - // "--log-level", - // "TRACE", - // "--log-calls" - // ], + "robotcode.extraArgs": [ + "--log", + "--log-level", + "DEBUG", + // "--log-calls" + ], "robotcode.robot.variableFiles": [ "${EXECDIR}/resources/testvars.yml" ], diff --git a/tests/robotcode/language_server/robotframework/parts/test_foldingrange.py b/tests/robotcode/language_server/robotframework/parts/test_foldingrange.py index 16f76019a..196cfd76d 100644 --- a/tests/robotcode/language_server/robotframework/parts/test_foldingrange.py +++ b/tests/robotcode/language_server/robotframework/parts/test_foldingrange.py @@ -1,4 +1,3 @@ -import asyncio from pathlib import Path from typing import Any, Iterable, Iterator, Tuple, Union @@ -63,16 +62,14 @@ def generate_foldingrange_test_id(params: Any) -> Any: ids=generate_foldingrange_test_id, scope="module", ) -@pytest.mark.asyncio() -async def test( +def test( regtest: RegTestFixtureEx, protocol: RobotLanguageServerProtocol, test_document: TextDocument, data: GeneratedTestData, ) -> None: - result = await asyncio.wait_for( - protocol.robot_folding_ranges.collect(protocol.robot_folding_ranges, test_document), 60 - ) + result = protocol.robot_folding_ranges.collect(protocol.robot_folding_ranges, test_document) + if result is not None: result = [ r diff --git a/tests/robotcode/language_server/robotframework/parts/test_goto_definition.py b/tests/robotcode/language_server/robotframework/parts/test_goto_definition.py index 23c2f748a..5dd523588 100644 --- a/tests/robotcode/language_server/robotframework/parts/test_goto_definition.py +++ b/tests/robotcode/language_server/robotframework/parts/test_goto_definition.py @@ -1,4 +1,3 @@ -import asyncio from pathlib import Path import pytest @@ -26,21 +25,14 @@ ids=generate_test_id, scope="module", ) -@pytest.mark.usefixtures("protocol") -@pytest.mark.asyncio() -async def test_definition( +def test_definition( regtest: RegTestFixtureEx, protocol: RobotLanguageServerProtocol, test_document: TextDocument, data: GeneratedTestData, ) -> None: - result = await asyncio.wait_for( - protocol.robot_goto.collect_definition( - protocol.robot_goto, - test_document, - Position(line=data.line, character=data.character), - ), - 60, + result = protocol.robot_goto.collect_definition( + protocol.robot_goto, test_document, Position(line=data.line, character=data.character) ) regtest.write(yaml.dump({"data": data, "result": split(result)})) diff --git a/tests/robotcode/language_server/robotframework/parts/test_goto_implementation.py b/tests/robotcode/language_server/robotframework/parts/test_goto_implementation.py index 55463f935..4f8357383 100644 --- a/tests/robotcode/language_server/robotframework/parts/test_goto_implementation.py +++ b/tests/robotcode/language_server/robotframework/parts/test_goto_implementation.py @@ -1,4 +1,3 @@ -import asyncio from pathlib import Path from typing import List, Union, cast @@ -50,21 +49,15 @@ def split( ids=generate_test_id, scope="module", ) -@pytest.mark.usefixtures("protocol") -@pytest.mark.asyncio() -async def test_implementation( +def test_implementation( regtest: RegTestFixtureEx, protocol: RobotLanguageServerProtocol, test_document: TextDocument, data: GeneratedTestData, ) -> None: - result = await asyncio.wait_for( - protocol.robot_goto.collect_implementation( - protocol.robot_goto, - test_document, - Position(line=data.line, character=data.character), - ), - 60, + result = protocol.robot_goto.collect_implementation( + protocol.robot_goto, + test_document, + Position(line=data.line, character=data.character), ) - regtest.write(yaml.dump({"data": data, "result": split(result)})) diff --git a/tests/robotcode/language_server/robotframework/parts/test_hover.py b/tests/robotcode/language_server/robotframework/parts/test_hover.py index a39a2b035..148e39876 100644 --- a/tests/robotcode/language_server/robotframework/parts/test_hover.py +++ b/tests/robotcode/language_server/robotframework/parts/test_hover.py @@ -46,9 +46,7 @@ def split(hover: Optional[Hover]) -> Optional[Hover]: ids=generate_test_id, scope="module", ) -@pytest.mark.usefixtures("protocol") -@pytest.mark.asyncio() -async def test( +def test( regtest: RegTestFixtureEx, protocol: RobotLanguageServerProtocol, test_document: TextDocument,