Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions plugin/core/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from functools import partial
from typing import Any
from typing import AsyncIterator
from typing import Awaitable
from typing import Callable
from typing import Coroutine
from typing import Protocol
Expand Down Expand Up @@ -182,6 +183,19 @@ async def gather_and_flatten_exceptions(*coros: Coroutine[Any, Any, list[Excepti
return exceptions


async def guard(untrusted: Awaitable[T], message: str = "Error calling async function") -> T | None:
"""
Run a coroutine that's not trusted.

Any exception is caught and logged. The only exception types this function may raise are of type BaseException.
"""
try:
return await untrusted
except Exception as ex:
exception_log(message, ex)
return None


class TaskContainer:
"""
A [mixin class](https://en.wikipedia.org/wiki/Mixin) for adding "fire-and-forget" functionality to a class for
Expand Down
34 changes: 24 additions & 10 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
from ..locationpicker import LocationPicker
from .aio import aclosing
from .aio import gather_and_flatten_exceptions
from .aio import guard
from .aio import run_on_asyncio_thread
from .aio import run_on_main_thread
from .aio import TaskContainer
Expand Down Expand Up @@ -178,8 +179,10 @@
from functools import partial
from pathlib import Path
from typing import Any
from typing import Awaitable
from typing import Callable
from typing import cast
from typing import Coroutine
from typing import Generator
from typing import Generic
from typing import Literal
Expand Down Expand Up @@ -1372,6 +1375,9 @@ def on_file_event_async(self, events: list[FileWatcherEvent]) -> None:

# --- misc methods -------------------------------------------------------------------------------------------------

def _guard(self, untrusted: Awaitable[None]) -> Coroutine[None, None, None]:
return guard(untrusted, f"{self._plugin.name} exception" if self._plugin else "exception")

def on_userprefs_changed_async(self) -> None:
self._redraw_config_status_async()
for sb in self.session_buffers_async():
Expand Down Expand Up @@ -1448,7 +1454,7 @@ async def initialize(
self._plugin.on_server_response_async('initialize', Response(-1, result))
await self.notify(Notification.initialized())
if self._plugin and isinstance(self._plugin, LspPlugin):
await self._plugin.on_initialized()
await self._guard(self._plugin.on_initialized())
self._maybe_send_did_change_configuration()
if execute_commands := self.get_capability('executeCommandProvider.commands'):
debug(f"{self.config.name}: Supported execute commands: {execute_commands}")
Expand Down Expand Up @@ -1514,6 +1520,7 @@ async def run_command(
if self._plugin:
if isinstance(self._plugin, LspPlugin):
if command_handler := self._plugin.get_command_handler(command_name):
# If the plugin handler raises, let the exception bubble up.
return await command_handler(command.get('arguments'))
else:
task: PackagedTask[LSPAny | Error | None] = Promise.packaged_task()
Expand Down Expand Up @@ -1675,6 +1682,7 @@ def open_untitled_buffer(flags: sublime.NewFileFlags) -> sublime.View:
if self._plugin:
if isinstance(self._plugin, LspPlugin):
if handler := self._plugin.get_uri_handler(scheme):
# If the plugin uri handler raises, let the exception bubble up.
sheet = await handler(uri, flags)
return self._on_sheet_for_uri_opened(sheet, uri, r)
else:
Expand Down Expand Up @@ -1791,7 +1799,7 @@ async def notify_plugin_on_session_buffer_change(self, session_buffer: SessionBu
if not self._plugin:
return
if isinstance(self._plugin, LspPlugin):
await self._plugin.on_text_changed(session_buffer)
await self._guard(self._plugin.on_text_changed(session_buffer))
else:
self._plugin.on_session_buffer_changed_async(session_buffer)

Expand Down Expand Up @@ -2638,7 +2646,7 @@ async def on_transport_close(self, exit_code: int, exception: Exception | None)
self._response_handlers.clear()
if self._plugin:
if isinstance(self._plugin, LspPlugin):
await self._plugin.on_session_end(exit_code, exception)
await self._guard(self._plugin.on_session_end(exit_code, exception))
else:
self._plugin.on_session_end_async(exit_code, exception)
self._plugin = None
Expand Down Expand Up @@ -2687,6 +2695,7 @@ def on_error(error: ResponseError) -> None:
self._plugin.on_pre_send_request_async(request_id, r)
elif self._plugin:
client_request = cast('ClientRequest', cast('object', {'method': r.method, 'params': r.params}))
# If the plugin raises an exception, let the exception bubble up.
self._plugin.on_pre_send_request_async(client_request, r.view)
r.params = cast('P', client_request['params'])
self._logger.outgoing_request(request_id, r.method, r.params)
Expand Down Expand Up @@ -2725,6 +2734,7 @@ def stream(self, r: Request[P, R]) -> CancellableInflightStreamingRequest[R]:
self._plugin.on_pre_send_request_async(request_id, r)
elif self._plugin:
client_request = cast('ClientRequest', cast('object', {'method': r.method, 'params': r.params}))
# If the plugin raises an exception, let the exception bubble up.
self._plugin.on_pre_send_request_async(client_request, r.view)
r.params = cast('P', client_request['params'])
self._logger.outgoing_request(request_id, r.method, r.params)
Expand Down Expand Up @@ -2792,10 +2802,14 @@ async def notify(self, notification: Notification[P]) -> None:
if self._plugin and isinstance(self._plugin, AbstractPlugin):
self._plugin.on_pre_send_notification_async(notification)
elif self._plugin:
client_notification = cast('ClientNotification',
cast('object', {'method': notification.method, 'params': notification.params}))
await self._plugin.on_pre_send_notification(client_notification)
notification.params = cast('P', client_notification['params'])
try:
client_notification = cast(
'ClientNotification', cast('object', {'method': notification.method, 'params': notification.params})
)
await self._plugin.on_pre_send_notification(client_notification)
notification.params = cast('P', client_notification['params'])
except Exception as ex:
exception_log(f"{self._plugin.name} exception", ex)
self._logger.outgoing_notification(notification.method, notification.params)
await self.send_payload(notification.to_payload())

Expand Down Expand Up @@ -2852,7 +2866,7 @@ async def deduce_payload(
elif self._plugin:
server_notification = cast('ServerNotification',
cast('object', {'method': method, 'params': result}))
await self._plugin.on_server_notification(server_notification)
await self._guard(self._plugin.on_server_notification(server_notification))
return res
elif "id" in payload:
response_id = payload["id"]
Expand All @@ -2868,7 +2882,7 @@ async def deduce_payload(
else:
server_response = cast('ServerResponse',
cast('object', {'method': method, 'result': response.result}))
await self._plugin.on_server_response(server_response)
await self._guard(self._plugin.on_server_response(server_response))
response.result = server_response['result']
return handler, response.result, None, None, None
else:
Expand Down Expand Up @@ -2905,7 +2919,7 @@ async def _handle_plugin_on_pre_send_response_async(
) -> Response[Any]:
if method and isinstance(self._plugin, LspPlugin):
obj = cast('ClientResponse', {'method': method, 'params': params, 'result': response.result})
await self._plugin.on_pre_send_response(obj)
await self._guard(self._plugin.on_pre_send_response(obj))
return response

def response_handler(
Expand Down
5 changes: 4 additions & 1 deletion plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,10 @@ def _on_selection_modified_debounced(self) -> None:
if code_lenses_enabled:
sv.session_buffer.resolve_visible_code_lenses_async(self.view)
if plugin := sv.session.plugin:
plugin.on_selection_modified_async(sv)
try:
plugin.on_selection_modified_async(sv)
except Exception as ex:
exception_log(f"{plugin.name} exception", ex)

async def on_post_save(self) -> list[BaseException | None]:
# Re-determine the URI; this time it's guaranteed to be a file because ST can only save files to a real
Expand Down