diff --git a/plugin/__init__.py b/plugin/__init__.py
index c0d309818..753dc7586 100644
--- a/plugin/__init__.py
+++ b/plugin/__init__.py
@@ -1,3 +1,5 @@
+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
@@ -6,6 +8,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
@@ -47,10 +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/api.py b/plugin/api.py
new file mode 100644
index 000000000..f0503ea3d
--- /dev/null
+++ b/plugin/api.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+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 .core.promise import Promise
+
+__all__ = [
+ 'APIHandler',
+ 'notification_handler',
+ 'request_handler',
+]
+
+HANDLER_MARKER = '__HANDLER_MARKER'
+
+# P represents the parameters *after* the 'self' argument
+P = TypeVar('P', bound=LSPAny)
+R = TypeVar('R', bound=LSPAny)
+
+
+class APIHandler:
+ """Trigger initialization of decorated API methods."""
+
+ 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(method, HANDLER_MARKER)), method)
+
+
+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.
+ """
+
+ def decorator(func: Callable[[Any, P], None]) -> Callable[[Any, P], None]:
+ setattr(func, HANDLER_MARKER, method)
+ return func
+
+ return decorator
+
+
+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.
+
+ Usage:
+ ```py
+ @request_handler('eslint/openDoc')
+ def on_open_doc(self, params: TextDocumentIdentifier) -> Promise[bool]:
+ ...
+ ```
+
+ 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.
+
+ :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[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))
+
+ 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..790430ce0 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,20 +18,18 @@
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)
self.message = params['message']
self.message_type = params.get('type', 4)
self.source = source
+ self._response_handled = False
- 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 +45,14 @@ 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: self._send_user_choice(resolve))
+ return promise
- def _send_user_choice(self, href: int = -1) -> None:
- if self.request_sent:
+ def _send_user_choice(self, resolve: ResolveFunc[MessageActionItem | None], href: int = -1) -> None:
+ if self._response_handled:
return
- self.request_sent = True
+ self._response_handled = True
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 83004b240..31e02d6a9 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
@@ -35,6 +36,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
@@ -45,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
@@ -66,10 +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 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
@@ -111,6 +118,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 +286,26 @@ 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]:
+ ...
+
+ @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]
@@ -855,26 +883,7 @@ def get_request_flags(self, session: Session) -> RequestFlags:
raise NotImplementedError()
-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.
- """
+class AbstractPlugin(APIHandler, metaclass=ABCMeta):
@classmethod
@abstractmethod
@@ -1080,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:
@@ -1317,13 +1328,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")
@@ -1363,7 +1367,7 @@ def check_applicable(self, sb: SessionBufferProtocol, *, suppress_requests: bool
_PARTIAL_RESULT_PROGRESS_PREFIX = "$ublime-partial-result-progress-"
-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:
@@ -1399,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:
"""
@@ -1708,12 +1713,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,25 +2174,29 @@ 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)
+ @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)
+ @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)
- 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)
@@ -2198,57 +2204,73 @@ 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())))
-
- 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})))
-
- 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))
- 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 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))
- 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:
+ return Promise.resolve(sublime.expand_variables(items, self._template_variables()))
+
+ @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})
+
+ @request_handler('workspace/codeLens/refresh')
+ def on_workspace_code_lens_refresh(self, _: None) -> Promise[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_response)
+ return Promise.resolve(None)
+
+ @request_handler('workspace/semanticTokens/refresh')
+ def on_workspace_semantic_tokens_refresh(self, _: None) -> Promise[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:
+ 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()
-
- 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))
- 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:
+
+ 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_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:
+ 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()
-
- 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))
- 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 m_textDocument_publishDiagnostics(self, params: PublishDiagnosticsParams) -> None:
- """handles the textDocument/publishDiagnostics notification"""
+
+ 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_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_response)
+ return Promise.resolve(None)
+
+ @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(
@@ -2267,8 +2289,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"])
@@ -2305,12 +2327,16 @@ 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()
- def m_client_unregisterCapability(self, params: UnregistrationParams, request_id: int | str) -> None:
- """handles the client/unregisterCapability request"""
+ def continue_after_response() -> None:
+ if new_workspace_diagnostics_provider:
+ self.do_workspace_diagnostics_async()
+
+ sublime.set_timeout_async(continue_after_response)
+ return Promise.resolve(None)
+
+ @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"]
@@ -2329,7 +2355,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:
@@ -2360,29 +2386,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:
@@ -2400,7 +2425,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']
@@ -2626,6 +2652,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
@@ -2633,14 +2660,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..2d13d3ad8 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
@@ -54,7 +56,7 @@
if TYPE_CHECKING:
- from tree_view import TreeViewSheet
+ from .tree_view import TreeViewSheet
_NO_DIAGNOSTICS_PLACEHOLDER = " No diagnostics. Well done!"
@@ -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)
@@ -407,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:
@@ -440,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)
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())
diff --git a/tests/test_session.py b/tests/test_session.py
index ea97e0ab9..5f22c1dd3 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,10 @@
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
from typing import Any, Generator
@@ -46,6 +51,22 @@ 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)
+
+ 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):