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):