Skip to content

Commit 276af72

Browse files
authored
feat: add API decorators (#2755)
1 parent 94e3d5f commit 276af72

File tree

9 files changed

+307
-174
lines changed

9 files changed

+307
-174
lines changed

plugin/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from .api import notification_handler
2+
from .api import request_handler
13
from .core.collections import DottedDict
24
from .core.css import css
35
from .core.edit import apply_text_edits
@@ -6,6 +8,7 @@
68
from .core.file_watcher import FileWatcherEventType
79
from .core.file_watcher import FileWatcherProtocol
810
from .core.file_watcher import register_file_watcher_implementation
11+
from .core.promise import Promise
912
from .core.protocol import Notification
1013
from .core.protocol import Request
1114
from .core.protocol import Response
@@ -47,10 +50,13 @@
4750
'MarkdownLangMap',
4851
'matches_pattern',
4952
'Notification',
53+
'notification_handler',
5054
'parse_uri',
55+
'Promise',
5156
'register_file_watcher_implementation',
5257
'register_plugin',
5358
'Request',
59+
'request_handler',
5460
'Response',
5561
'Session',
5662
'SessionBufferProtocol',

plugin/api.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from __future__ import annotations
2+
from ..protocol import LSPAny
3+
from .core.protocol import Response
4+
from .core.types import method2attr
5+
from functools import wraps
6+
from typing import Any, Callable, TypeVar, TYPE_CHECKING
7+
import inspect
8+
9+
if TYPE_CHECKING:
10+
from .core.promise import Promise
11+
12+
__all__ = [
13+
'APIHandler',
14+
'notification_handler',
15+
'request_handler',
16+
]
17+
18+
HANDLER_MARKER = '__HANDLER_MARKER'
19+
20+
# P represents the parameters *after* the 'self' argument
21+
P = TypeVar('P', bound=LSPAny)
22+
R = TypeVar('R', bound=LSPAny)
23+
24+
25+
class APIHandler:
26+
"""Trigger initialization of decorated API methods."""
27+
28+
def __init__(self) -> None:
29+
super().__init__()
30+
for _, method in inspect.getmembers(self, inspect.ismethod):
31+
if hasattr(method, HANDLER_MARKER):
32+
# Set method with transformed name on the class instance.
33+
setattr(self, method2attr(getattr(method, HANDLER_MARKER)), method)
34+
35+
36+
def notification_handler(method: str) -> Callable[[Callable[[Any, P], None]], Callable[[Any, P], None]]:
37+
"""Decorator to mark a method as a handler for a specific LSP notification.
38+
39+
Usage:
40+
```py
41+
@notification_handler('eslint/status')
42+
def on_eslint_status(self, params: str) -> None:
43+
...
44+
```
45+
46+
The decorated method will be called with the notification parameters whenever the specified
47+
notification is received from the language server. Notification handlers do not return a value.
48+
49+
:param method: The LSP notification method name (e.g., 'eslint/status').
50+
:returns: A decorator that registers the function as a notification handler.
51+
"""
52+
53+
def decorator(func: Callable[[Any, P], None]) -> Callable[[Any, P], None]:
54+
setattr(func, HANDLER_MARKER, method)
55+
return func
56+
57+
return decorator
58+
59+
60+
def request_handler(
61+
method: str
62+
) -> Callable[[Callable[[Any, P], Promise[R]]], Callable[[Any, P, int], Promise[Response[R]]]]:
63+
"""Decorator to mark a method as a handler for a specific LSP request.
64+
65+
Usage:
66+
```py
67+
@request_handler('eslint/openDoc')
68+
def on_open_doc(self, params: TextDocumentIdentifier) -> Promise[bool]:
69+
...
70+
```
71+
72+
The decorated method will be called with the request parameters whenever the specified
73+
request is received from the language server. The method must return a Promise that resolves
74+
to the response value. The framework will automatically send it back to the server.
75+
76+
:param method: The LSP request method name (e.g., 'eslint/openDoc').
77+
:returns: A decorator that registers the function as a request handler.
78+
"""
79+
80+
def decorator(func: Callable[[Any, P], Promise[R]]) -> Callable[[Any, P, int], Promise[Response[R]]]:
81+
82+
@wraps(func)
83+
def wrapper(self: Any, params: P, request_id: int) -> Promise[Response[Any]]:
84+
promise = func(self, params)
85+
return promise.then(lambda result: Response(request_id, result))
86+
87+
setattr(wrapper, HANDLER_MARKER, method)
88+
return wrapper
89+
90+
return decorator
Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from __future__ import annotations
2+
from ...protocol import MessageActionItem
23
from ...protocol import MessageType
34
from ...protocol import ShowMessageRequestParams
4-
from .protocol import Response
5-
from .sessions import Session
5+
from .promise import PackagedTask, Promise, ResolveFunc
66
from .views import show_lsp_popup
77
from .views import text2html
8-
from typing import Any
98
import sublime
109

1110

@@ -19,20 +18,18 @@
1918

2019

2120
class MessageRequestHandler:
22-
def __init__(
23-
self, view: sublime.View, session: Session, request_id: Any, params: ShowMessageRequestParams, source: str
24-
) -> None:
25-
self.session = session
26-
self.request_id = request_id
27-
self.request_sent = False
21+
def __init__(self, view: sublime.View, params: ShowMessageRequestParams, source: str) -> None:
2822
self.view = view
2923
self.actions = params.get("actions", [])
3024
self.action_titles = list(action.get("title") for action in self.actions)
3125
self.message = params['message']
3226
self.message_type = params.get('type', 4)
3327
self.source = source
28+
self._response_handled = False
3429

35-
def show(self) -> None:
30+
def show(self) -> Promise[MessageActionItem | None]:
31+
task: PackagedTask[MessageActionItem | None] = Promise.packaged_task()
32+
promise, resolve = task
3633
formatted: list[str] = []
3734
formatted.append(f"<h2>{self.source}</h2>")
3835
icon = ICONS.get(self.message_type, '')
@@ -48,15 +45,14 @@ def show(self) -> None:
4845
location=self.view.layout_to_text(self.view.viewport_position()),
4946
css=sublime.load_resource("Packages/LSP/notification.css"),
5047
wrapper_class='notification',
51-
on_navigate=self._send_user_choice,
52-
on_hide=self._send_user_choice)
48+
on_navigate=lambda href: self._send_user_choice(resolve, href),
49+
on_hide=lambda: self._send_user_choice(resolve))
50+
return promise
5351

54-
def _send_user_choice(self, href: int = -1) -> None:
55-
if self.request_sent:
52+
def _send_user_choice(self, resolve: ResolveFunc[MessageActionItem | None], href: int = -1) -> None:
53+
if self._response_handled:
5654
return
57-
self.request_sent = True
55+
self._response_handled = True
5856
self.view.hide_popup()
5957
index = int(href)
60-
param = self.actions[index] if index != -1 else None
61-
response = Response(self.request_id, param)
62-
self.session.send_response(response)
58+
resolve(self.actions[index] if index != -1 else None)

plugin/core/rpc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# only used for LSP-* packages out in the wild that now import from "private" modules
22
# TODO: Announce removal and remove this import
3-
from .sessions import method2attr # noqa
3+
from .types import method2attr # noqa

0 commit comments

Comments
 (0)