From 0a03aafb64d61377afd3cced14d4fb5c38ac8afe Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 9 Feb 2026 23:05:17 +0100 Subject: [PATCH 01/18] feat: add APIDecorator --- plugin/__init__.py | 4 ++ plugin/core/api_decorator.py | 78 ++++++++++++++++++++++++++ plugin/core/message_request_handler.py | 30 ++++------ plugin/core/rpc.py | 2 +- plugin/core/sessions.py | 32 +++++++---- plugin/core/types.py | 7 +++ plugin/core/windows.py | 9 ++- tests/test_message_request_handler.py | 23 -------- 8 files changed, 129 insertions(+), 56 deletions(-) create mode 100644 plugin/core/api_decorator.py delete mode 100644 tests/test_message_request_handler.py diff --git a/plugin/__init__.py b/plugin/__init__.py index c0d309818..6fed2212f 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1,3 +1,4 @@ +from .core.api_decorator import APIDecorator from .core.collections import DottedDict from .core.css import css from .core.edit import apply_text_edits @@ -6,6 +7,7 @@ from .core.file_watcher import FileWatcherEventType from .core.file_watcher import FileWatcherProtocol from .core.file_watcher import register_file_watcher_implementation +from .core.promise import Promise from .core.protocol import Notification from .core.protocol import Request from .core.protocol import Response @@ -32,6 +34,7 @@ __all__ = [ '__version__', 'AbstractPlugin', + 'APIDecorator', 'apply_text_edits', 'ClientConfig', 'css', @@ -48,6 +51,7 @@ 'matches_pattern', 'Notification', 'parse_uri', + 'Promise', 'register_file_watcher_implementation', 'register_plugin', 'Request', diff --git a/plugin/core/api_decorator.py b/plugin/core/api_decorator.py new file mode 100644 index 000000000..bef884511 --- /dev/null +++ b/plugin/core/api_decorator.py @@ -0,0 +1,78 @@ +from __future__ import annotations +from ...protocol import LSPAny +from .protocol import Response +from .types import method2attr +from functools import wraps +from typing import Any, Callable, TypeVar, TYPE_CHECKING +from typing_extensions import ParamSpec + +if TYPE_CHECKING: + from .promise import Promise + +__all__ = [ + 'APIDecorator', +] + +HANDLER_MARKER = '__HANDLER_MARKER' + +T = TypeVar('T') +Params = ParamSpec('Params') +# P represents the parameters *after* the 'self' argument +P = TypeVar('P', bound=LSPAny) +R = TypeVar('R', bound=LSPAny) + + +class APIDecorator: + """Decorate plugin class methods to handle server initiated requests and notifications. + + 1. Ensure plugin class is decorated with `APIDecorator.initialize`. + 2. Add `APIDecorator.request('...')` and/or `APIDecorator.notification('...')` decorates on class methods. + + Notification handlers receive one parameter containing notification parameters. + + Request handlers receive one parameter containing request parameters and return Promise that should be resolved + with response value. All requests must receive a response. + """ + + @staticmethod + def initialize(_class: type[T]) -> type[T]: + original_init = _class.__init__ + + @wraps(original_init) + def init_wrapper(self: T, *args: Params.args, **kwargs: Params.kwargs) -> None: + original_init(self, *args, **kwargs) + for attr in dir(self): + if (func := getattr(self, attr)) and callable(func) and hasattr(func, HANDLER_MARKER): + # Set method with transformed name on the class instance. + setattr(self, method2attr(getattr(func, HANDLER_MARKER)), func) + + _class.__init__ = init_wrapper + return _class + + @staticmethod + def notification_handler(method: str) -> Callable[[Callable[[Any, P], None]], Callable[[Any, P], None]]: + """Mark the decorated function as a "notification" message handler.""" + + def decorator(func: Callable[[Any, P], None]) -> Callable[[Any, P], None]: + setattr(func, HANDLER_MARKER, method) + return func + + return decorator + + @staticmethod + def request_handler( + method: str + ) -> Callable[[Callable[[Any, P], Promise[R]]], Callable[[Any, P, int], Promise[Response[Any]]]]: + """Mark the decorated function as a "request" message handler.""" + + def decorator(func: Callable[[Any, P], Promise[R]]) -> Callable[[Any, P, int], Promise[Response[Any]]]: + + @wraps(func) + def wrapper(self: Any, params: P, request_id: int) -> Promise[Response[Any]]: + promise = func(self, params) + return promise.then(lambda result: Response(request_id, result)) + + setattr(wrapper, HANDLER_MARKER, method) + return wrapper + + return decorator diff --git a/plugin/core/message_request_handler.py b/plugin/core/message_request_handler.py index 88a327888..e3491bef1 100644 --- a/plugin/core/message_request_handler.py +++ b/plugin/core/message_request_handler.py @@ -1,11 +1,10 @@ from __future__ import annotations +from ...protocol import MessageActionItem from ...protocol import MessageType from ...protocol import ShowMessageRequestParams -from .protocol import Response -from .sessions import Session +from .promise import PackagedTask, Promise, ResolveFunc from .views import show_lsp_popup from .views import text2html -from typing import Any import sublime @@ -19,12 +18,7 @@ class MessageRequestHandler: - def __init__( - self, view: sublime.View, session: Session, request_id: Any, params: ShowMessageRequestParams, source: str - ) -> None: - self.session = session - self.request_id = request_id - self.request_sent = False + def __init__(self, view: sublime.View, params: ShowMessageRequestParams, source: str) -> None: self.view = view self.actions = params.get("actions", []) self.action_titles = list(action.get("title") for action in self.actions) @@ -32,7 +26,9 @@ def __init__( self.message_type = params.get('type', 4) self.source = source - def show(self) -> None: + def show(self) -> Promise[MessageActionItem | None]: + task: PackagedTask[MessageActionItem | None] = Promise.packaged_task() + promise, resolve = task formatted: list[str] = [] formatted.append(f"

{self.source}

") icon = ICONS.get(self.message_type, '') @@ -48,15 +44,11 @@ def show(self) -> None: location=self.view.layout_to_text(self.view.viewport_position()), css=sublime.load_resource("Packages/LSP/notification.css"), wrapper_class='notification', - on_navigate=self._send_user_choice, - on_hide=self._send_user_choice) + on_navigate=lambda href: self._send_user_choice(resolve, href), + on_hide=lambda href: self._send_user_choice(resolve, href)) + return promise - def _send_user_choice(self, href: int = -1) -> None: - if self.request_sent: - return - self.request_sent = True + def _send_user_choice(self, resolve: ResolveFunc[MessageActionItem | None], href: int = -1) -> None: self.view.hide_popup() index = int(href) - param = self.actions[index] if index != -1 else None - response = Response(self.request_id, param) - self.session.send_response(response) + resolve(self.actions[index] if index != -1 else None) diff --git a/plugin/core/rpc.py b/plugin/core/rpc.py index b22e687bd..366d0dc65 100644 --- a/plugin/core/rpc.py +++ b/plugin/core/rpc.py @@ -1,3 +1,3 @@ # only used for LSP-* packages out in the wild that now import from "private" modules # TODO: Announce removal and remove this import -from .sessions import method2attr # noqa +from .types import method2attr # noqa diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 5ae494cf6..a01db8b5d 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -35,6 +35,7 @@ from ...protocol import LSPErrorCodes from ...protocol import LSPObject from ...protocol import MarkupKind +from ...protocol import MessageActionItem from ...protocol import PrepareSupportDefaultBehavior from ...protocol import PreviousResultId from ...protocol import ProgressParams @@ -70,6 +71,7 @@ from ..diagnostics import DiagnosticsIdentifier from ..diagnostics import DiagnosticsStorage from ..diagnostics import WORKSPACE_DIAGNOSTICS_RETRIGGER_DELAY +from .api_decorator import APIDecorator from .constants import RequestFlags from .constants import MARKO_MD_PARSER_VERSION from .constants import SEMANTIC_TOKENS_MAP @@ -111,6 +113,7 @@ from .types import debounced from .types import diff from .types import DocumentSelector_ +from .types import method2attr from .types import method_to_capability from .types import SemanticToken from .types import SettingsRegistration @@ -278,6 +281,12 @@ def on_post_exit_async(self, session: Session, exit_code: int, exception: Except """ raise NotImplementedError() + @abstractmethod + def handle_message_request( + self, config_name: str, params: ShowMessageRequestParams + ) -> Promise[MessageActionItem | None]: + ... + def _int_enum_to_list(e: type[IntEnum]) -> list[int]: return [v.value for v in e] @@ -1314,13 +1323,6 @@ def print_to_status_bar(error: ResponseError) -> None: sublime.status_message(error["message"]) -def method2attr(method: str) -> str: - # window/messageRequest -> m_window_messageRequest - # $/progress -> m___progress - # client/registerCapability -> m_client_registerCapability - return 'm_' + ''.join(map(lambda c: c if c.isalpha() else '_', method)) - - class _RegistrationData: __slots__ = ("registration_id", "capability_path", "registration_path", "options", "session_buffers", "selector") @@ -1360,6 +1362,7 @@ def check_applicable(self, sb: SessionBufferProtocol, *, suppress_requests: bool _PARTIAL_RESULT_PROGRESS_PREFIX = "$ublime-partial-result-progress-" +@APIDecorator.initialize class Session(TransportCallbacks): def __init__(self, manager: Manager, logger: Logger, workspace_folders: list[WorkspaceFolder], @@ -2169,9 +2172,11 @@ def _on_workspace_diagnostics_error_async(self, identifier: DiagnosticsIdentifie # --- server request handlers -------------------------------------------------------------------------------------- - def m_window_showMessageRequest(self, params: ShowMessageRequestParams, request_id: int | str) -> None: - """handles the window/showMessageRequest request""" - self.call_manager('handle_message_request', self, params, request_id) + @APIDecorator.request_handler('window/showMessageRequest') + def on_show_message_request(self, params: ShowMessageRequestParams) -> Promise[MessageActionItem | None]: + if mgr := self.manager(): + return mgr.handle_message_request(self.config.name, params) + return Promise.resolve(None) def m_window_showMessage(self, params: ShowMessageParams) -> None: """handles the window/showMessage notification""" @@ -2623,6 +2628,7 @@ def deduce_payload( def on_payload(self, payload: dict[str, Any]) -> None: handler, result, req_id, typestr, _method = self.deduce_payload(payload) if handler: + result_promise: Promise[Response[Any]] | None = None try: if req_id is None: # notification or response @@ -2630,14 +2636,18 @@ def on_payload(self, payload: dict[str, Any]) -> None: else: # request try: - handler(result, req_id) + result_promise = cast('Promise[Response[Any]] | None', handler(result, req_id)) except Error as err: self.send_error_response(req_id, err) + return except Exception as ex: self.send_error_response(req_id, Error.from_exception(ex)) raise except Exception as err: exception_log(f"Error handling {typestr}", err) + return + if isinstance(result_promise, Promise): + result_promise.then(self.send_response) def response_handler( self, response_id: int, response: dict[str, Any] diff --git a/plugin/core/types.py b/plugin/core/types.py index c68a36e45..18124df7a 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -503,6 +503,13 @@ def matches(file_operation_filter: FileOperationFilter) -> bool: } +def method2attr(method: str) -> str: + # window/messageRequest -> m_window_messageRequest + # $/progress -> m___progress + # client/registerCapability -> m_client_registerCapability + return 'm_' + ''.join(map(lambda c: c if c.isalpha() else '_', method)) + + def method_to_capability(method: str) -> tuple[str, str]: """ Given a method, returns the corresponding capability path, and the associated path to stash the registration key. diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 06e83d45b..f0eacbec8 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -2,6 +2,7 @@ from ...protocol import Diagnostic from ...protocol import DocumentUri from ...protocol import LogMessageParams +from ...protocol import MessageActionItem from ...protocol import MessageType from ...protocol import ShowMessageParams from ...protocol import ShowMessageRequestParams @@ -19,6 +20,7 @@ from .panels import MAX_LOG_LINES_LIMIT_ON from .panels import PanelManager from .panels import PanelName +from .promise import Promise from .protocol import Error from .protocol import Point from .sessions import AbstractViewListener @@ -328,9 +330,12 @@ def _create_logger(self, config_name: str) -> Logger: router_logger.append(logger(self, config_name)) return router_logger - def handle_message_request(self, session: Session, params: ShowMessageRequestParams, request_id: int | str) -> None: + def handle_message_request( + self, config_name: str, params: ShowMessageRequestParams + ) -> Promise[MessageActionItem | None]: if view := self._window.active_view(): - MessageRequestHandler(view, session, request_id, params, session.config.name).show() + return MessageRequestHandler(view, params, config_name).show() + return Promise.resolve(None) def restart_sessions_async(self, config_names: list[str]) -> None: self._end_sessions_async(config_names) diff --git a/tests/test_message_request_handler.py b/tests/test_message_request_handler.py deleted file mode 100644 index 4e0e1d52f..000000000 --- a/tests/test_message_request_handler.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations -from LSP.plugin.core.message_request_handler import MessageRequestHandler -from test_mocks import MockSession -import sublime -import unittest - - -class MessageRequestHandlerTest(unittest.TestCase): - def test_show_popup(self): - window = sublime.active_window() - view = window.active_view() - session = MockSession() - params = { - 'type': 1, - 'message': 'hello', - 'actions': [ - {'title': "abc"}, - {'title': "def"} - ] - } - handler = MessageRequestHandler(view, session, "1", params, 'lsp server') - handler.show() - self.assertTrue(view.is_popup_visible()) From 26a67d27fd01f438c2fda26b1b65e6b0c4bbc4ef Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 09:02:43 +0100 Subject: [PATCH 02/18] update mock --- tests/test_session.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_session.py b/tests/test_session.py index 9ff99bdfa..e0d0003e6 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,5 +1,6 @@ from __future__ import annotations from LSP.plugin.core.collections import DottedDict +from LSP.plugin.core.edit import Promise from LSP.plugin.core.protocol import Error from LSP.plugin.core.sessions import get_initialize_params from LSP.plugin.core.sessions import Logger @@ -9,6 +10,8 @@ from LSP.plugin.core.workspace import WorkspaceFolder from LSP.protocol import Diagnostic from LSP.protocol import DocumentUri +from LSP.protocol import MessageActionItem +from LSP.protocol import ShowMessageRequestParams from LSP.protocol import TextDocumentSyncKind from test_mocks import TEST_CONFIG from typing import Any, Generator @@ -47,6 +50,11 @@ def on_post_exit_async(self, session: Session, exit_code: int, exception: Except def on_diagnostics_updated(self) -> None: pass + def handle_message_request( + self, config_name: str, params: ShowMessageRequestParams + ) -> Promise[MessageActionItem | None]: + return Promise.resolve(None) + class MockLogger(Logger): From 6b9f891fe2933830b8f5047738b94d70585c2cfb Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 09:03:17 +0100 Subject: [PATCH 03/18] better name --- plugin/core/sessions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index a01db8b5d..d59affede 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -2173,7 +2173,7 @@ def _on_workspace_diagnostics_error_async(self, identifier: DiagnosticsIdentifie # --- server request handlers -------------------------------------------------------------------------------------- @APIDecorator.request_handler('window/showMessageRequest') - def on_show_message_request(self, params: ShowMessageRequestParams) -> Promise[MessageActionItem | None]: + def on_window_show_message_request(self, params: ShowMessageRequestParams) -> Promise[MessageActionItem | None]: if mgr := self.manager(): return mgr.handle_message_request(self.config.name, params) return Promise.resolve(None) From 87762345ecfedc3b0e708d8ea248622f6b9ab9dd Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 09:06:04 +0100 Subject: [PATCH 04/18] docs --- plugin/core/api_decorator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/core/api_decorator.py b/plugin/core/api_decorator.py index bef884511..66df8adbf 100644 --- a/plugin/core/api_decorator.py +++ b/plugin/core/api_decorator.py @@ -25,13 +25,13 @@ class APIDecorator: """Decorate plugin class methods to handle server initiated requests and notifications. - 1. Ensure plugin class is decorated with `APIDecorator.initialize`. - 2. Add `APIDecorator.request('...')` and/or `APIDecorator.notification('...')` decorates on class methods. + 1. Ensure class is decorated with `APIDecorator.initialize`. + 2. Add `APIDecorator.request('...')` and/or `APIDecorator.notification('...')` decorators on chosen class methods. - Notification handlers receive one parameter containing notification parameters. + Notification handlers receive one parameter with notification parameters. - Request handlers receive one parameter containing request parameters and return Promise that should be resolved - with response value. All requests must receive a response. + Request handlers receive one parameter with request parameters and return a Promise that should be resolved + with the response value. All requests must receive a response. """ @staticmethod From ee79cfb430999f1a195f97f4ac50d8b842d10e1b Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 09:06:28 +0100 Subject: [PATCH 05/18] docs --- plugin/core/api_decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/api_decorator.py b/plugin/core/api_decorator.py index 66df8adbf..1fd30ee21 100644 --- a/plugin/core/api_decorator.py +++ b/plugin/core/api_decorator.py @@ -23,7 +23,7 @@ class APIDecorator: - """Decorate plugin class methods to handle server initiated requests and notifications. + """Decorate class methods to handle server initiated requests and notifications. 1. Ensure class is decorated with `APIDecorator.initialize`. 2. Add `APIDecorator.request('...')` and/or `APIDecorator.notification('...')` decorators on chosen class methods. From 0df3460468168f039fd6ec20e50b7cc0b9e13d8c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 19:42:28 +0100 Subject: [PATCH 06/18] finish updating --- plugin/__init__.py | 6 +- plugin/core/api_decorator.py | 100 ++++++++++++++--------- plugin/core/sessions.py | 153 ++++++++++++++++++----------------- plugin/core/windows.py | 22 ++--- 4 files changed, 152 insertions(+), 129 deletions(-) diff --git a/plugin/__init__.py b/plugin/__init__.py index 6fed2212f..c61b66830 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1,4 +1,5 @@ -from .core.api_decorator import APIDecorator +from .core.api_decorator import notification_handler +from .core.api_decorator import request_handler from .core.collections import DottedDict from .core.css import css from .core.edit import apply_text_edits @@ -34,7 +35,6 @@ __all__ = [ '__version__', 'AbstractPlugin', - 'APIDecorator', 'apply_text_edits', 'ClientConfig', 'css', @@ -50,11 +50,13 @@ 'MarkdownLangMap', 'matches_pattern', 'Notification', + 'notification_handler', 'parse_uri', 'Promise', 'register_file_watcher_implementation', 'register_plugin', 'Request', + 'request_handler', 'Response', 'Session', 'SessionBufferProtocol', diff --git a/plugin/core/api_decorator.py b/plugin/core/api_decorator.py index 1fd30ee21..411224f5c 100644 --- a/plugin/core/api_decorator.py +++ b/plugin/core/api_decorator.py @@ -10,7 +10,9 @@ from .promise import Promise __all__ = [ - 'APIDecorator', + 'initialize_api', + 'notification_handler', + 'request_handler', ] HANDLER_MARKER = '__HANDLER_MARKER' @@ -22,57 +24,75 @@ R = TypeVar('R', bound=LSPAny) -class APIDecorator: - """Decorate class methods to handle server initiated requests and notifications. +def initialize_api(_class: type[T]) -> type[T]: + """Internal decorator used for processing decorated methods.""" - 1. Ensure class is decorated with `APIDecorator.initialize`. - 2. Add `APIDecorator.request('...')` and/or `APIDecorator.notification('...')` decorators on chosen class methods. + original_init = _class.__init__ - Notification handlers receive one parameter with notification parameters. + @wraps(original_init) + def init_wrapper(self: T, *args: Params.args, **kwargs: Params.kwargs) -> None: + original_init(self, *args, **kwargs) + for attr in dir(self): + if (func := getattr(self, attr)) and callable(func) and hasattr(func, HANDLER_MARKER): + # Set method with transformed name on the class instance. + setattr(self, method2attr(getattr(func, HANDLER_MARKER)), func) - Request handlers receive one parameter with request parameters and return a Promise that should be resolved - with the response value. All requests must receive a response. + _class.__init__ = init_wrapper + return _class + + +def notification_handler(method: str) -> Callable[[Callable[[Any, P], None]], Callable[[Any, P], None]]: + """Decorator to mark a method as a handler for a specific LSP notification. + + Usage: + ```py + @notification_handler('eslint/status') + def on_eslint_status(self, params: str) -> None: + ... + ``` + + The decorated method will be called with the notification parameters whenever the specified + notification is received from the language server. Notification handlers do not return a value. + + :param method: The LSP notification method name (e.g., 'eslint/status'). + :returns: A decorator that registers the function as a notification handler. """ - @staticmethod - def initialize(_class: type[T]) -> type[T]: - original_init = _class.__init__ + def decorator(func: Callable[[Any, P], None]) -> Callable[[Any, P], None]: + setattr(func, HANDLER_MARKER, method) + return func - @wraps(original_init) - def init_wrapper(self: T, *args: Params.args, **kwargs: Params.kwargs) -> None: - original_init(self, *args, **kwargs) - for attr in dir(self): - if (func := getattr(self, attr)) and callable(func) and hasattr(func, HANDLER_MARKER): - # Set method with transformed name on the class instance. - setattr(self, method2attr(getattr(func, HANDLER_MARKER)), func) + return decorator - _class.__init__ = init_wrapper - return _class - @staticmethod - def notification_handler(method: str) -> Callable[[Callable[[Any, P], None]], Callable[[Any, P], None]]: - """Mark the decorated function as a "notification" message handler.""" +def request_handler( + method: str +) -> Callable[[Callable[[Any, P], Promise[R]]], Callable[[Any, P, int], Promise[Response[R]]]]: + """Decorator to mark a method as a handler for a specific LSP request. - def decorator(func: Callable[[Any, P], None]) -> Callable[[Any, P], None]: - setattr(func, HANDLER_MARKER, method) - return func + Usage: + ```py + @request_handler('eslint/openDoc') + def on_hover(self, params: TextDocumentIdentifier) -> Promise[bool]: + ... + ``` - return decorator + The decorated method will be called with the request parameters whenever the specified + request is received from the language server. The method must return a Promise that resolves + to the response value. The framework will automatically send it back to the server. - @staticmethod - def request_handler( - method: str - ) -> Callable[[Callable[[Any, P], Promise[R]]], Callable[[Any, P, int], Promise[Response[Any]]]]: - """Mark the decorated function as a "request" message handler.""" + :param method: The LSP request method name (e.g., 'eslint/openDoc'). + :returns: A decorator that registers the function as a request handler. + """ - def decorator(func: Callable[[Any, P], Promise[R]]) -> Callable[[Any, P, int], Promise[Response[Any]]]: + def decorator(func: Callable[[Any, P], Promise[R]]) -> Callable[[Any, P, int], Promise[Response[R]]]: - @wraps(func) - def wrapper(self: Any, params: P, request_id: int) -> Promise[Response[Any]]: - promise = func(self, params) - return promise.then(lambda result: Response(request_id, result)) + @wraps(func) + def wrapper(self: Any, params: P, request_id: int) -> Promise[Response[Any]]: + promise = func(self, params) + return promise.then(lambda result: Response(request_id, result)) - setattr(wrapper, HANDLER_MARKER, method) - return wrapper + setattr(wrapper, HANDLER_MARKER, method) + return wrapper - return decorator + return decorator diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index d3b7c6f20..63368ea02 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1,5 +1,6 @@ from __future__ import annotations from ...protocol import ApplyWorkspaceEditParams +from ...protocol import ApplyWorkspaceEditResult from ...protocol import ClientCapabilities from ...protocol import CodeAction from ...protocol import CodeActionKind @@ -46,6 +47,7 @@ from ...protocol import SemanticTokenModifiers from ...protocol import SemanticTokenTypes from ...protocol import ShowDocumentParams +from ...protocol import ShowDocumentResult from ...protocol import ShowMessageParams from ...protocol import ShowMessageRequestParams from ...protocol import SignatureHelpTriggerKind @@ -67,11 +69,14 @@ from ...protocol import WorkspaceDiagnosticReport from ...protocol import WorkspaceDocumentDiagnosticReport from ...protocol import WorkspaceEdit +from ...protocol import WorkspaceFolder as LspWorkspaceFolder from ...protocol import WorkspaceFullDocumentDiagnosticReport from ..diagnostics import DiagnosticsIdentifier from ..diagnostics import DiagnosticsStorage from ..diagnostics import WORKSPACE_DIAGNOSTICS_RETRIGGER_DELAY -from .api_decorator import APIDecorator +from .api_decorator import initialize_api +from .api_decorator import notification_handler +from .api_decorator import request_handler from .constants import RequestFlags from .constants import MARKO_MD_PARSER_VERSION from .constants import SEMANTIC_TOKENS_MAP @@ -287,6 +292,20 @@ def handle_message_request( ) -> Promise[MessageActionItem | None]: ... + @abstractmethod + def handle_show_message( + self, config_name: str, params: ShowMessageParams + ) -> Promise[MessageActionItem | None]: + ... + + @abstractmethod + def handle_log_message(self, config_name: str, params: LogMessageParams) -> None: + ... + + @abstractmethod + def handle_stderr_log(self, config_name: str, message: str) -> None: + ... + def _int_enum_to_list(e: type[IntEnum]) -> list[int]: return [v.value for v in e] @@ -861,26 +880,8 @@ def get_request_flags(self, session: Session) -> RequestFlags: raise NotImplementedError() +@initialize_api class AbstractPlugin(metaclass=ABCMeta): - """ - Inherit from this class to handle non-standard requests and notifications. - Given a request/notification, replace the non-alphabetic characters with an underscore, and prepend it with "m_". - This will be the name of your method. - For instance, to implement the non-standard eslint/openDoc request, define the Python method - - def m_eslint_openDoc(self, params, request_id): - session = self.weaksession() - if session: - webbrowser.open_tab(params['url']) - session.send_response(Response(request_id, None)) - - To handle the non-standard eslint/status notification, define the Python method - - def m_eslint_status(self, params): - pass - - To understand how this works, see the __getattr__ method of the Session class. - """ @classmethod @abstractmethod @@ -1362,7 +1363,7 @@ def check_applicable(self, sb: SessionBufferProtocol, *, suppress_requests: bool _PARTIAL_RESULT_PROGRESS_PREFIX = "$ublime-partial-result-progress-" -@APIDecorator.initialize +@initialize_api class Session(TransportCallbacks): def __init__(self, manager: Manager, logger: Logger, workspace_folders: list[WorkspaceFolder], @@ -1708,12 +1709,9 @@ def _get_global_ignore_globs(self, root_path: str) -> list[str]: ] return folder_excludes + file_excludes + ['**/node_modules/**'] - def call_manager(self, method: str, *args: Any) -> None: - if mgr := self.manager(): - getattr(mgr, method)(*args) - def on_stderr_message(self, message: str) -> None: - self.call_manager('handle_stderr_log', self, message) + if mgr := self.manager(): + mgr.handle_stderr_log(self.config.name, message) self._logger.stderr_message(message) def _supports_workspace_folders(self) -> bool: @@ -2172,27 +2170,30 @@ def _on_workspace_diagnostics_error_async(self, identifier: DiagnosticsIdentifie # --- server request handlers -------------------------------------------------------------------------------------- - @APIDecorator.request_handler('window/showMessageRequest') + @request_handler('window/showMessageRequest') def on_window_show_message_request(self, params: ShowMessageRequestParams) -> Promise[MessageActionItem | None]: if mgr := self.manager(): return mgr.handle_message_request(self.config.name, params) return Promise.resolve(None) - def m_window_showMessage(self, params: ShowMessageParams) -> None: - """handles the window/showMessage notification""" - self.call_manager('handle_show_message', self, params) + @request_handler('window/showMessage') + def on_window_show_message(self, params: ShowMessageParams) -> Promise[MessageActionItem | None]: + if mgr := self.manager(): + mgr.handle_show_message(self.config.name, params) + return Promise.resolve(None) - def m_window_logMessage(self, params: LogMessageParams) -> None: - """handles the window/logMessage notification""" - self.call_manager('handle_log_message', self, params) + @notification_handler('window/logMessage') + def on_window_log_message(self, params: LogMessageParams) -> None: + if mgr := self.manager(): + mgr.handle_log_message(self.config.name, params) - def m_workspace_workspaceFolders(self, params: None, request_id: int | str) -> None: - """handles the workspace/workspaceFolders request""" - self.send_response(Response(request_id, [wf.to_lsp() for wf in self._workspace_folders])) + @request_handler('workspace/workspaceFolders') + def on_workspace_workspace_folders(self, _: None) -> Promise[list[LspWorkspaceFolder]]: + return Promise.resolve([wf.to_lsp() for wf in self._workspace_folders]) - def m_workspace_configuration(self, params: ConfigurationParams, request_id: int | str) -> None: - """handles the workspace/configuration request""" - items: list[Any] = [] + @request_handler('workspace/configuration') + def on_workspace_configuration(self, params: ConfigurationParams) -> Promise[list[LSPAny]]: + items: list[LSPAny] = [] requested_items = params.get("items") or [] for requested_item in requested_items: configuration = self.config.settings.copy(requested_item.get('section') or None) @@ -2200,25 +2201,24 @@ def m_workspace_configuration(self, params: ConfigurationParams, request_id: int items.append(self._plugin.on_workspace_configuration(requested_item, configuration)) else: items.append(configuration) - self.send_response(Response(request_id, sublime.expand_variables(items, self._template_variables()))) + return Promise.resolve(sublime.expand_variables(items, self._template_variables())) - def m_workspace_applyEdit(self, params: ApplyWorkspaceEditParams, request_id: int | str) -> None: - """handles the workspace/applyEdit request""" - self.apply_workspace_edit_async(params.get('edit', {}), label=params.get('label')) \ - .then(lambda _: self.send_response(Response(request_id, {"applied": True}))) + @request_handler('workspace/applyEdit') + def on_workspace_apply_edit(self, params: ApplyWorkspaceEditParams) -> Promise[ApplyWorkspaceEditResult]: + return self.apply_workspace_edit_async(params.get('edit', {}), label=params.get('label')) \ + .then(lambda _: {"applied": True}) - def m_workspace_codeLens_refresh(self, params: None, request_id: int | str) -> None: - """handles the workspace/codeLens/refresh request""" - self.send_response(Response(request_id, None)) + @request_handler('workspace/codeLens/refresh') + def on_workspace_code_lens_refresh(self, _: None) -> Promise[None]: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: sv.session_buffer.do_code_lenses_async(sv.view) for sv in not_visible_session_views: sv.session_buffer.set_code_lenses_pending_refresh() + return Promise.resolve(None) - def m_workspace_semanticTokens_refresh(self, params: None, request_id: int | str) -> None: - """handles the workspace/semanticTokens/refresh request""" - self.send_response(Response(request_id, None)) + @request_handler('workspace/semanticTokens/refresh') + def on_workspace_semantic_tokens_refresh(self, _: None) -> Promise[None]: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: if sv.get_request_flags() & RequestFlags.SEMANTIC_TOKENS: @@ -2227,10 +2227,10 @@ def m_workspace_semanticTokens_refresh(self, params: None, request_id: int | str sv.session_buffer.set_semantic_tokens_pending_refresh() for sv in not_visible_session_views: sv.session_buffer.set_semantic_tokens_pending_refresh() + return Promise.resolve(None) - def m_workspace_inlayHint_refresh(self, params: None, request_id: int | str) -> None: - """handles the workspace/inlayHint/refresh request""" - self.send_response(Response(request_id, None)) + @request_handler('workspace/inlayHint/refresh') + def on_workspace_inlay_hint_refresh(self, _: None) -> Promise[None]: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: if sv.get_request_flags() & RequestFlags.INLAY_HINT: @@ -2239,18 +2239,19 @@ def m_workspace_inlayHint_refresh(self, params: None, request_id: int | str) -> sv.session_buffer.set_inlay_hints_pending_refresh() for sv in not_visible_session_views: sv.session_buffer.set_inlay_hints_pending_refresh() + return Promise.resolve(None) - def m_workspace_diagnostic_refresh(self, params: None, request_id: int | str) -> None: - """handles the workspace/diagnostic/refresh request""" - self.send_response(Response(request_id, None)) + @request_handler('workspace/diagnostic/refresh') + def on_workspace_diagnostic_refresh(self, _: None) -> Promise[None]: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: sv.session_buffer.do_document_diagnostic_async(sv.view, sv.view.change_count(), forced_update=True) for sv in not_visible_session_views: sv.session_buffer.set_document_diagnostic_pending_refresh() + return Promise.resolve(None) - def m_textDocument_publishDiagnostics(self, params: PublishDiagnosticsParams) -> None: - """handles the textDocument/publishDiagnostics notification""" + @notification_handler('textDocument/publishDiagnostics') + def on_text_document_publish_diagnostics(self, params: PublishDiagnosticsParams) -> None: self.handle_diagnostics_async(params['uri'], None, None, params['diagnostics']) def handle_diagnostics_async( @@ -2269,8 +2270,8 @@ def handle_diagnostics_async( self._publish_diagnostics_to_session_buffer_async( session_buffer, self.diagnostics.get_diagnostics_for_uri(uri), version) - def m_client_registerCapability(self, params: RegistrationParams, request_id: int | str) -> None: - """handles the client/registerCapability request""" + @request_handler('client/registerCapability') + def on_client_register_capability(self, params: RegistrationParams) -> Promise[None]: new_workspace_diagnostics_provider = False for registration in params["registrations"]: capability_path, registration_path = method_to_capability(registration["method"]) @@ -2307,12 +2308,12 @@ def m_client_registerCapability(self, params: RegistrationParams, request_id: in if capability_path == "didChangeWatchedFilesProvider": capability_options = cast('DidChangeWatchedFilesRegistrationOptions', options) self.register_file_system_watchers(registration_id, capability_options['watchers']) - self.send_response(Response(request_id, None)) if new_workspace_diagnostics_provider: self.do_workspace_diagnostics_async() + return Promise.resolve(None) - def m_client_unregisterCapability(self, params: UnregistrationParams, request_id: int | str) -> None: - """handles the client/unregisterCapability request""" + @request_handler('client/unregisterCapability') + def on_client_unregister_capability(self, params: UnregistrationParams) -> Promise[None]: unregistrations = params["unregisterations"] # typo in the official specification for unregistration in unregistrations: registration_id = unregistration["id"] @@ -2331,7 +2332,7 @@ def m_client_unregisterCapability(self, params: UnregistrationParams, request_id if isinstance(discarded, dict): for sv in self.session_views_async(): sv.on_capability_removed_async(registration_id, discarded) - self.send_response(Response(request_id, None)) + return Promise.resolve(None) def register_file_system_watchers(self, registration_id: str, watchers: list[FileSystemWatcher]) -> None: if not self._watcher_impl: @@ -2362,29 +2363,28 @@ def unregister_file_system_watchers(self, registration_id: str) -> None: for file_watcher in file_watchers: file_watcher.destroy() - def m_window_showDocument(self, params: ShowDocumentParams, request_id: int | str) -> None: - """handles the window/showDocument request""" + @request_handler('window/showDocument') + def on_window_show_document(self, params: ShowDocumentParams) -> Promise[ShowDocumentResult]: uri = params.get("uri") - def success(b: None | bool | sublime.View) -> None: + def success(b: None | bool | sublime.View) -> ShowDocumentResult: if isinstance(b, bool): pass elif isinstance(b, sublime.View): b = b.is_valid() else: b = False - self.send_response(Response(request_id, {"success": b})) + return ({"success": b}) if params.get("external"): - success(open_externally(uri, bool(params.get("takeFocus")))) - else: - # TODO: ST API does not allow us to say "do not focus this new view" - self.open_uri_async(uri, params.get("selection")).then(success) + return Promise.resolve(success(open_externally(uri, bool(params.get("takeFocus"))))) + # TODO: ST API does not allow us to say "do not focus this new view" + return self.open_uri_async(uri, params.get("selection")).then(success) - def m_window_workDoneProgress_create(self, params: WorkDoneProgressCreateParams, request_id: int | str) -> None: - """handles the window/workDoneProgress/create request""" + @request_handler('window/workDoneProgress/create') + def on_window_work_done_progress_create(self, params: WorkDoneProgressCreateParams) -> Promise[None]: self._progress[params['token']] = None - self.send_response(Response(request_id, None)) + return Promise.resolve(None) def _invoke_views(self, request: Request[Any, Any], method: str, *args: Any) -> None: if request.view: @@ -2402,7 +2402,8 @@ def _create_window_progress_reporter(self, token: ProgressToken, value: WorkDone message=value.get("message") ) - def m___progress(self, params: ProgressParams) -> None: + @notification_handler('$/progress') + def on_progress(self, params: ProgressParams) -> None: """handles the $/progress notification""" token = params['token'] value = params['value'] diff --git a/plugin/core/windows.py b/plugin/core/windows.py index f0eacbec8..590048d5c 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -412,24 +412,24 @@ def destroy(self) -> None: self.panel_manager.destroy_output_panels() self.panel_manager = None - def handle_log_message(self, session: Session, params: LogMessageParams) -> None: + def handle_log_message(self, config_name: str, params: LogMessageParams) -> None: if not userprefs().log_debug: return message_type = params['type'] level = MESSAGE_TYPE_LEVELS[message_type] message = params['message'] - print(f"{session.config.name}: {level}: {message}") + print(f"{config_name}: {level}: {message}") if message_type == MessageType.Error: - self.window.status_message(f"{session.config.name}: {message}") + self.window.status_message(f"{config_name}: {message}") - def handle_stderr_log(self, session: Session, message: str) -> None: - self.handle_server_message_async(session.config.name, message) + def handle_stderr_log(self, config_name: str, message: str) -> None: + self.handle_server_message_async(config_name, message) - def handle_server_message_async(self, server_name: str, message: str) -> None: - sublime.set_timeout(lambda: self.log_server_message(server_name, message)) + def handle_server_message_async(self, config_name: str, message: str) -> None: + sublime.set_timeout(lambda: self.log_server_message(config_name, message)) - def log_server_message(self, prefix: str, message: str) -> None: - self._server_log.append((prefix, message)) + def log_server_message(self, config_name: str, message: str) -> None: + self._server_log.append((config_name, message)) list_len = len(self._server_log) max_lines = self.get_log_lines_limit() if list_len >= max_lines: @@ -445,10 +445,10 @@ def is_log_lines_limit_enabled(self) -> bool: panel = self.panel_manager and self.panel_manager.get_panel(PanelName.Log) return bool(panel and panel.settings().get(LOG_LINES_LIMIT_SETTING_NAME, True)) - def handle_show_message(self, session: Session, params: ShowMessageParams) -> None: + def handle_show_message(self, config_name: str, params: ShowMessageParams) -> None: level = MESSAGE_TYPE_LEVELS[params['type']] message = params['message'] - msg = f"{session.config.name}: {level}: {message}" + msg = f"{config_name}: {level}: {message}" debug(msg) self.window.status_message(msg) From 9a86234647a864db71a9dd345ce2e06d1ada19c0 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 19:46:20 +0100 Subject: [PATCH 07/18] better name? --- plugin/core/api_decorator.py | 4 ++-- plugin/core/sessions.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/core/api_decorator.py b/plugin/core/api_decorator.py index 411224f5c..0dde97995 100644 --- a/plugin/core/api_decorator.py +++ b/plugin/core/api_decorator.py @@ -10,7 +10,7 @@ from .promise import Promise __all__ = [ - 'initialize_api', + 'initialize_api_decorators', 'notification_handler', 'request_handler', ] @@ -24,7 +24,7 @@ R = TypeVar('R', bound=LSPAny) -def initialize_api(_class: type[T]) -> type[T]: +def initialize_api_decorators(_class: type[T]) -> type[T]: """Internal decorator used for processing decorated methods.""" original_init = _class.__init__ diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 63368ea02..436a7ead2 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -74,7 +74,7 @@ from ..diagnostics import DiagnosticsIdentifier from ..diagnostics import DiagnosticsStorage from ..diagnostics import WORKSPACE_DIAGNOSTICS_RETRIGGER_DELAY -from .api_decorator import initialize_api +from .api_decorator import initialize_api_decorators from .api_decorator import notification_handler from .api_decorator import request_handler from .constants import RequestFlags @@ -880,7 +880,7 @@ def get_request_flags(self, session: Session) -> RequestFlags: raise NotImplementedError() -@initialize_api +@initialize_api_decorators class AbstractPlugin(metaclass=ABCMeta): @classmethod @@ -1363,7 +1363,7 @@ def check_applicable(self, sb: SessionBufferProtocol, *, suppress_requests: bool _PARTIAL_RESULT_PROGRESS_PREFIX = "$ublime-partial-result-progress-" -@initialize_api +@initialize_api_decorators class Session(TransportCallbacks): def __init__(self, manager: Manager, logger: Logger, workspace_folders: list[WorkspaceFolder], From f7c73f632c7a70d2fad20fd7c5ad575622c776ea Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 19:48:05 +0100 Subject: [PATCH 08/18] update mock --- tests/test_session.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_session.py b/tests/test_session.py index 0f1a6d9f4..f435b0464 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -10,7 +10,9 @@ from LSP.plugin.core.workspace import WorkspaceFolder from LSP.protocol import Diagnostic from LSP.protocol import DocumentUri +from LSP.protocol import LogMessageParams from LSP.protocol import MessageActionItem +from LSP.protocol import ShowMessageParams from LSP.protocol import ShowMessageRequestParams from LSP.protocol import TextDocumentSyncKind from test_mocks import TEST_CONFIG @@ -54,6 +56,18 @@ def handle_message_request( ) -> Promise[MessageActionItem | None]: return Promise.resolve(None) + def handle_show_message( + self, config_name: str, params: ShowMessageParams + ) -> Promise[MessageActionItem | None]: + return Promise.resolve(None) + + def handle_log_message(self, config_name: str, params: LogMessageParams) -> None: + ... + + def handle_stderr_log(self, config_name: str, message: str) -> None: + ... + + class MockLogger(Logger): From e20207627468abec406369e43afbeac260cc4f4a Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 19:48:21 +0100 Subject: [PATCH 09/18] blank --- tests/test_session.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_session.py b/tests/test_session.py index f435b0464..5f22c1dd3 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -68,7 +68,6 @@ def handle_stderr_log(self, config_name: str, message: str) -> None: ... - class MockLogger(Logger): def stderr_message(self, message: str) -> None: From aa780eb9389543be103473c61a3faed8c227c6ab Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 19:49:17 +0100 Subject: [PATCH 10/18] make import relative --- plugin/core/windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 590048d5c..2d13d3ad8 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -56,7 +56,7 @@ if TYPE_CHECKING: - from tree_view import TreeViewSheet + from .tree_view import TreeViewSheet _NO_DIAGNOSTICS_PLACEHOLDER = " No diagnostics. Well done!" From 383b922fb2b0f2d6cc44ac25279761c38f5143cc Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 10 Feb 2026 21:18:09 +0100 Subject: [PATCH 11/18] fix wrong decorator --- plugin/core/sessions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 436a7ead2..c45afe569 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -2176,11 +2176,10 @@ def on_window_show_message_request(self, params: ShowMessageRequestParams) -> Pr return mgr.handle_message_request(self.config.name, params) return Promise.resolve(None) - @request_handler('window/showMessage') - def on_window_show_message(self, params: ShowMessageParams) -> Promise[MessageActionItem | None]: + @notification_handler('window/showMessage') + def on_window_show_message(self, params: ShowMessageParams) -> None: if mgr := self.manager(): mgr.handle_show_message(self.config.name, params) - return Promise.resolve(None) @notification_handler('window/logMessage') def on_window_log_message(self, params: LogMessageParams) -> None: From a4ba115853ea6dab5d481492a0057093f79c3a66 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 11 Feb 2026 20:17:35 +0100 Subject: [PATCH 12/18] retain old semantics --- plugin/core/sessions.py | 72 ++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index c45afe569..92c5587e1 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -2209,44 +2209,60 @@ def on_workspace_apply_edit(self, params: ApplyWorkspaceEditParams) -> Promise[A @request_handler('workspace/codeLens/refresh') def on_workspace_code_lens_refresh(self, _: None) -> Promise[None]: - visible_session_views, not_visible_session_views = self.session_views_by_visibility() - for sv in visible_session_views: - sv.session_buffer.do_code_lenses_async(sv.view) - for sv in not_visible_session_views: - sv.session_buffer.set_code_lenses_pending_refresh() + + def continue_after_return() -> None: + visible_session_views, not_visible_session_views = self.session_views_by_visibility() + for sv in visible_session_views: + sv.session_buffer.do_code_lenses_async(sv.view) + for sv in not_visible_session_views: + sv.session_buffer.set_code_lenses_pending_refresh() + + sublime.set_timeout_async(continue_after_return) return Promise.resolve(None) @request_handler('workspace/semanticTokens/refresh') def on_workspace_semantic_tokens_refresh(self, _: None) -> Promise[None]: - visible_session_views, not_visible_session_views = self.session_views_by_visibility() - for sv in visible_session_views: - if sv.get_request_flags() & RequestFlags.SEMANTIC_TOKENS: - sv.session_buffer.do_semantic_tokens_async(sv.view) - else: + + def continue_after_return() -> None: + visible_session_views, not_visible_session_views = self.session_views_by_visibility() + for sv in visible_session_views: + if sv.get_request_flags() & RequestFlags.SEMANTIC_TOKENS: + sv.session_buffer.do_semantic_tokens_async(sv.view) + else: + sv.session_buffer.set_semantic_tokens_pending_refresh() + for sv in not_visible_session_views: sv.session_buffer.set_semantic_tokens_pending_refresh() - for sv in not_visible_session_views: - sv.session_buffer.set_semantic_tokens_pending_refresh() + + sublime.set_timeout_async(continue_after_return) return Promise.resolve(None) @request_handler('workspace/inlayHint/refresh') def on_workspace_inlay_hint_refresh(self, _: None) -> Promise[None]: - visible_session_views, not_visible_session_views = self.session_views_by_visibility() - for sv in visible_session_views: - if sv.get_request_flags() & RequestFlags.INLAY_HINT: - sv.session_buffer.do_inlay_hints_async(sv.view) - else: + + def continue_after_return() -> None: + visible_session_views, not_visible_session_views = self.session_views_by_visibility() + for sv in visible_session_views: + if sv.get_request_flags() & RequestFlags.INLAY_HINT: + sv.session_buffer.do_inlay_hints_async(sv.view) + else: + sv.session_buffer.set_inlay_hints_pending_refresh() + for sv in not_visible_session_views: sv.session_buffer.set_inlay_hints_pending_refresh() - for sv in not_visible_session_views: - sv.session_buffer.set_inlay_hints_pending_refresh() + + sublime.set_timeout_async(continue_after_return) return Promise.resolve(None) @request_handler('workspace/diagnostic/refresh') def on_workspace_diagnostic_refresh(self, _: None) -> Promise[None]: - visible_session_views, not_visible_session_views = self.session_views_by_visibility() - for sv in visible_session_views: - sv.session_buffer.do_document_diagnostic_async(sv.view, sv.view.change_count(), forced_update=True) - for sv in not_visible_session_views: - sv.session_buffer.set_document_diagnostic_pending_refresh() + + def continue_after_return() -> None: + visible_session_views, not_visible_session_views = self.session_views_by_visibility() + for sv in visible_session_views: + sv.session_buffer.do_document_diagnostic_async(sv.view, sv.view.change_count(), forced_update=True) + for sv in not_visible_session_views: + sv.session_buffer.set_document_diagnostic_pending_refresh() + + sublime.set_timeout_async(continue_after_return) return Promise.resolve(None) @notification_handler('textDocument/publishDiagnostics') @@ -2307,8 +2323,12 @@ def on_client_register_capability(self, params: RegistrationParams) -> Promise[N if capability_path == "didChangeWatchedFilesProvider": capability_options = cast('DidChangeWatchedFilesRegistrationOptions', options) self.register_file_system_watchers(registration_id, capability_options['watchers']) - if new_workspace_diagnostics_provider: - self.do_workspace_diagnostics_async() + + def continue_after_return() -> None: + if new_workspace_diagnostics_provider: + self.do_workspace_diagnostics_async() + + sublime.set_timeout_async(continue_after_return) return Promise.resolve(None) @request_handler('client/unregisterCapability') From ef4b4f8fa5107994ddb63c6437cae6cf5c81705a Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 11 Feb 2026 21:01:05 +0100 Subject: [PATCH 13/18] fix message request handling --- plugin/core/message_request_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/core/message_request_handler.py b/plugin/core/message_request_handler.py index e3491bef1..790430ce0 100644 --- a/plugin/core/message_request_handler.py +++ b/plugin/core/message_request_handler.py @@ -25,6 +25,7 @@ def __init__(self, view: sublime.View, params: ShowMessageRequestParams, source: self.message = params['message'] self.message_type = params.get('type', 4) self.source = source + self._response_handled = False def show(self) -> Promise[MessageActionItem | None]: task: PackagedTask[MessageActionItem | None] = Promise.packaged_task() @@ -45,10 +46,13 @@ def show(self) -> Promise[MessageActionItem | None]: css=sublime.load_resource("Packages/LSP/notification.css"), wrapper_class='notification', on_navigate=lambda href: self._send_user_choice(resolve, href), - on_hide=lambda href: self._send_user_choice(resolve, href)) + on_hide=lambda: self._send_user_choice(resolve)) return promise def _send_user_choice(self, resolve: ResolveFunc[MessageActionItem | None], href: int = -1) -> None: + if self._response_handled: + return + self._response_handled = True self.view.hide_popup() index = int(href) resolve(self.actions[index] if index != -1 else None) From bf4da754ec5236df1e461c07a2db19cafb3e881c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 11 Feb 2026 21:09:48 +0100 Subject: [PATCH 14/18] doc --- plugin/core/api_decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/api_decorator.py b/plugin/core/api_decorator.py index 0dde97995..0cb4168e4 100644 --- a/plugin/core/api_decorator.py +++ b/plugin/core/api_decorator.py @@ -73,7 +73,7 @@ def request_handler( Usage: ```py @request_handler('eslint/openDoc') - def on_hover(self, params: TextDocumentIdentifier) -> Promise[bool]: + def on_open_doc(self, params: TextDocumentIdentifier) -> Promise[bool]: ... ``` From 95991e1c22ea8fc1d46d2290d7995973b4d6e520 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 15 Feb 2026 09:57:48 +0100 Subject: [PATCH 15/18] use normal inheritance to init decorators --- plugin/core/api_decorator.py | 26 +++++++++----------------- plugin/core/sessions.py | 11 ++++++----- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/plugin/core/api_decorator.py b/plugin/core/api_decorator.py index 0cb4168e4..dae81d916 100644 --- a/plugin/core/api_decorator.py +++ b/plugin/core/api_decorator.py @@ -4,41 +4,33 @@ from .types import method2attr from functools import wraps from typing import Any, Callable, TypeVar, TYPE_CHECKING -from typing_extensions import ParamSpec +import inspect if TYPE_CHECKING: from .promise import Promise __all__ = [ - 'initialize_api_decorators', + 'APIHandler', 'notification_handler', 'request_handler', ] HANDLER_MARKER = '__HANDLER_MARKER' -T = TypeVar('T') -Params = ParamSpec('Params') # P represents the parameters *after* the 'self' argument P = TypeVar('P', bound=LSPAny) R = TypeVar('R', bound=LSPAny) -def initialize_api_decorators(_class: type[T]) -> type[T]: - """Internal decorator used for processing decorated methods.""" +class APIHandler: + """Trigger initialization of decorated API methods.""" - original_init = _class.__init__ - - @wraps(original_init) - def init_wrapper(self: T, *args: Params.args, **kwargs: Params.kwargs) -> None: - original_init(self, *args, **kwargs) - for attr in dir(self): - if (func := getattr(self, attr)) and callable(func) and hasattr(func, HANDLER_MARKER): + def __init__(self) -> None: + super().__init__() + for _, method in inspect.getmembers(self, inspect.ismethod): + if hasattr(method, HANDLER_MARKER): # Set method with transformed name on the class instance. - setattr(self, method2attr(getattr(func, HANDLER_MARKER)), func) - - _class.__init__ = init_wrapper - return _class + setattr(self, method2attr(getattr(method, HANDLER_MARKER)), method) def notification_handler(method: str) -> Callable[[Callable[[Any, P], None]], Callable[[Any, P], None]]: diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index be2b2de73..cde2c2e7d 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -74,7 +74,7 @@ from ..diagnostics import DiagnosticsIdentifier from ..diagnostics import DiagnosticsStorage from ..diagnostics import WORKSPACE_DIAGNOSTICS_RETRIGGER_DELAY -from .api_decorator import initialize_api_decorators +from .api_decorator import APIHandler from .api_decorator import notification_handler from .api_decorator import request_handler from .constants import RequestFlags @@ -883,8 +883,7 @@ def get_request_flags(self, session: Session) -> RequestFlags: raise NotImplementedError() -@initialize_api_decorators -class AbstractPlugin(metaclass=ABCMeta): +class AbstractPlugin(APIHandler, metaclass=ABCMeta): @classmethod @abstractmethod @@ -1090,6 +1089,8 @@ def __init__(self, weaksession: weakref.ref[Session]) -> None: :param weaksession: A weak reference to the Session. You can grab a strong reference through self.weaksession(), but don't hold on to that reference. """ + + super().__init__() self.weaksession = weaksession def on_settings_changed(self, settings: DottedDict) -> None: @@ -1366,8 +1367,7 @@ def check_applicable(self, sb: SessionBufferProtocol, *, suppress_requests: bool _PARTIAL_RESULT_PROGRESS_PREFIX = "$ublime-partial-result-progress-" -@initialize_api_decorators -class Session(TransportCallbacks): +class Session(APIHandler, TransportCallbacks['dict[str, Any]']): def __init__(self, manager: Manager, logger: Logger, workspace_folders: list[WorkspaceFolder], config: ClientConfig, plugin_class: type[AbstractPlugin] | None) -> None: @@ -1403,6 +1403,7 @@ def __init__(self, manager: Manager, logger: Logger, workspace_folders: list[Wor self._semantic_tokens_map = get_semantic_tokens_map(config.semantic_tokens) self._is_executing_refactoring_command = False self._logged_unsupported_commands: set[str] = set() + super().__init__() def __getattr__(self, name: str) -> Any: """ From 766897ad17d19a7ddee838afd8b6dcdb566fabe7 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 16 Feb 2026 19:46:43 +0100 Subject: [PATCH 16/18] rename file to api.py --- plugin/__init__.py | 4 ++-- plugin/core/{api_decorator.py => api.py} | 0 plugin/core/sessions.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename plugin/core/{api_decorator.py => api.py} (100%) diff --git a/plugin/__init__.py b/plugin/__init__.py index c61b66830..67987cb12 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1,5 +1,5 @@ -from .core.api_decorator import notification_handler -from .core.api_decorator import request_handler +from .core.api import notification_handler +from .core.api import request_handler from .core.collections import DottedDict from .core.css import css from .core.edit import apply_text_edits diff --git a/plugin/core/api_decorator.py b/plugin/core/api.py similarity index 100% rename from plugin/core/api_decorator.py rename to plugin/core/api.py diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index cde2c2e7d..3ae3a2bcb 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -74,9 +74,9 @@ from ..diagnostics import DiagnosticsIdentifier from ..diagnostics import DiagnosticsStorage from ..diagnostics import WORKSPACE_DIAGNOSTICS_RETRIGGER_DELAY -from .api_decorator import APIHandler -from .api_decorator import notification_handler -from .api_decorator import request_handler +from .api import APIHandler +from .api import notification_handler +from .api import request_handler from .constants import RequestFlags from .constants import MARKO_MD_PARSER_VERSION from .constants import SEMANTIC_TOKENS_MAP From 6184125e900ee75143c0056d4ad981f7c986f99f Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 16 Feb 2026 19:51:05 +0100 Subject: [PATCH 17/18] plugin/api.py --- plugin/__init__.py | 4 ++-- plugin/{core => }/api.py | 8 ++++---- plugin/core/sessions.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename plugin/{core => }/api.py (95%) diff --git a/plugin/__init__.py b/plugin/__init__.py index 67987cb12..753dc7586 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -1,5 +1,5 @@ -from .core.api import notification_handler -from .core.api import request_handler +from .api import notification_handler +from .api import request_handler from .core.collections import DottedDict from .core.css import css from .core.edit import apply_text_edits diff --git a/plugin/core/api.py b/plugin/api.py similarity index 95% rename from plugin/core/api.py rename to plugin/api.py index dae81d916..f0503ea3d 100644 --- a/plugin/core/api.py +++ b/plugin/api.py @@ -1,13 +1,13 @@ from __future__ import annotations -from ...protocol import LSPAny -from .protocol import Response -from .types import method2attr +from ..protocol import LSPAny +from .core.protocol import Response +from .core.types import method2attr from functools import wraps from typing import Any, Callable, TypeVar, TYPE_CHECKING import inspect if TYPE_CHECKING: - from .promise import Promise + from .core.promise import Promise __all__ = [ 'APIHandler', diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 3ae3a2bcb..24956f116 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -74,9 +74,9 @@ from ..diagnostics import DiagnosticsIdentifier from ..diagnostics import DiagnosticsStorage from ..diagnostics import WORKSPACE_DIAGNOSTICS_RETRIGGER_DELAY -from .api import APIHandler -from .api import notification_handler -from .api import request_handler +from ..api import APIHandler +from ..api import notification_handler +from ..api import request_handler from .constants import RequestFlags from .constants import MARKO_MD_PARSER_VERSION from .constants import SEMANTIC_TOKENS_MAP From 776dd3b7dbf9eaa26f9acbd1e70ed7969d11550c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 16 Feb 2026 22:57:24 +0100 Subject: [PATCH 18/18] continue_after_response --- plugin/core/sessions.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 24956f116..31e02d6a9 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -2214,20 +2214,20 @@ def on_workspace_apply_edit(self, params: ApplyWorkspaceEditParams) -> Promise[A @request_handler('workspace/codeLens/refresh') def on_workspace_code_lens_refresh(self, _: None) -> Promise[None]: - def continue_after_return() -> None: + def continue_after_response() -> None: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: sv.session_buffer.do_code_lenses_async(sv.view) for sv in not_visible_session_views: sv.session_buffer.set_code_lenses_pending_refresh() - sublime.set_timeout_async(continue_after_return) + sublime.set_timeout_async(continue_after_response) return Promise.resolve(None) @request_handler('workspace/semanticTokens/refresh') def on_workspace_semantic_tokens_refresh(self, _: None) -> Promise[None]: - def continue_after_return() -> None: + def continue_after_response() -> None: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: if sv.get_request_flags() & RequestFlags.SEMANTIC_TOKENS: @@ -2237,13 +2237,13 @@ def continue_after_return() -> None: for sv in not_visible_session_views: sv.session_buffer.set_semantic_tokens_pending_refresh() - sublime.set_timeout_async(continue_after_return) + sublime.set_timeout_async(continue_after_response) return Promise.resolve(None) @request_handler('workspace/inlayHint/refresh') def on_workspace_inlay_hint_refresh(self, _: None) -> Promise[None]: - def continue_after_return() -> None: + def continue_after_response() -> None: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: if sv.get_request_flags() & RequestFlags.INLAY_HINT: @@ -2253,20 +2253,20 @@ def continue_after_return() -> None: for sv in not_visible_session_views: sv.session_buffer.set_inlay_hints_pending_refresh() - sublime.set_timeout_async(continue_after_return) + sublime.set_timeout_async(continue_after_response) return Promise.resolve(None) @request_handler('workspace/diagnostic/refresh') def on_workspace_diagnostic_refresh(self, _: None) -> Promise[None]: - def continue_after_return() -> None: + def continue_after_response() -> None: visible_session_views, not_visible_session_views = self.session_views_by_visibility() for sv in visible_session_views: sv.session_buffer.do_document_diagnostic_async(sv.view, sv.view.change_count(), forced_update=True) for sv in not_visible_session_views: sv.session_buffer.set_document_diagnostic_pending_refresh() - sublime.set_timeout_async(continue_after_return) + sublime.set_timeout_async(continue_after_response) return Promise.resolve(None) @notification_handler('textDocument/publishDiagnostics') @@ -2328,11 +2328,11 @@ def on_client_register_capability(self, params: RegistrationParams) -> Promise[N capability_options = cast('DidChangeWatchedFilesRegistrationOptions', options) self.register_file_system_watchers(registration_id, capability_options['watchers']) - def continue_after_return() -> None: + def continue_after_response() -> None: if new_workspace_diagnostics_provider: self.do_workspace_diagnostics_async() - sublime.set_timeout_async(continue_after_return) + sublime.set_timeout_async(continue_after_response) return Promise.resolve(None) @request_handler('client/unregisterCapability')