From 48d0751461fc8a53601a3adabfb78c27c32890f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Sun, 8 Dec 2024 21:33:28 +0100 Subject: [PATCH 1/4] Just add event loop The one who originally wrote the code is Raoul Wols Co-authored-by: Raoul Wols --- __init__.py | 9 +++++++ event_loop/__init__.py | 54 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 __init__.py create mode 100644 event_loop/__init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..2c9f87cd2 --- /dev/null +++ b/__init__.py @@ -0,0 +1,9 @@ +import sublime_plugin +from .event_loop import setup_event_loop, shutdown_event_loop + +setup_event_loop() + + +class EventLoopListener(sublime_plugin.EventListener): + def on_exit(self) -> None: + shutdown_event_loop() diff --git a/event_loop/__init__.py b/event_loop/__init__.py new file mode 100644 index 000000000..616d4a3d3 --- /dev/null +++ b/event_loop/__init__.py @@ -0,0 +1,54 @@ +from __future__ import annotations +import asyncio +from threading import Thread +from typing import Any, Awaitable +from ..plugin.core.logging import debug + +__loop: asyncio.AbstractEventLoop | None = None +__thread: Thread | None = None +__active_tasks: set[asyncio.Task] = set() + + +def run_future(future: Awaitable): + global __loop, __active_tasks + if __loop: + task = asyncio.ensure_future(future, loop=__loop) + __active_tasks.add(task) + task.add_done_callback(__active_tasks.discard) # Remove once done + __loop.call_soon_threadsafe(lambda: task) + + +def setup_event_loop(): + debug('loop: starting') + global __loop + global __thread + if __loop: + debug('loop: already created') + return + __loop = asyncio.new_event_loop() + __thread = Thread(target=__loop.run_forever) + __thread.start() + debug("loop: started") + + +def shutdown_event_loop(): + debug("loop: stopping") + global __loop + global __thread + + if not __loop: + debug('no loop to shutdown.') + + def __shutdown(): + for task in asyncio.all_tasks(): + task.cancel() + asyncio.get_event_loop().stop() + + if __loop and __thread: + __loop.call_soon_threadsafe(__shutdown) + __thread.join() + __loop.run_until_complete(__loop.shutdown_asyncgens()) + __loop.close() + __loop = None + __thread = None + debug("loop: stopped") From bd7cdae757d0c84caae503f8815efbceed8a68e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Sun, 8 Dec 2024 21:34:57 +0100 Subject: [PATCH 2/4] Add example on how to use the event loop --- delete_this_example.py | 27 ++++++++++++++++++ view_async.py | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 delete_this_example.py create mode 100644 view_async.py diff --git a/delete_this_example.py b/delete_this_example.py new file mode 100644 index 000000000..69cdc84c8 --- /dev/null +++ b/delete_this_example.py @@ -0,0 +1,27 @@ +import sublime_plugin +from LSP.event_loop import run_future +from .view_async import open_view, save_view +from pathlib import Path + +class LspExampleAsyncCommand(sublime_plugin.TextCommand): + def run(self, edit): + print('1. this will be printed first') + run_future(self.open_edit_save_and_close_file()) + print('2. This will printed second') + + async def open_edit_save_and_close_file(self): + print('3. than this') + w = self.view.window() + if not w: + return + file_name = self.view.file_name() + if not file_name: + return + readme_file = str((Path(file_name) / Path('../README.md')).resolve()) + view = await open_view(readme_file, w) + view.run_command("append", { + 'characters': "LspExampleAsyncCommand added this" + '\n\n', + }) + await save_view(view) + # the view is saved at this point and safe to be closed + view.close() diff --git a/view_async.py b/view_async.py new file mode 100644 index 000000000..3c917bdc2 --- /dev/null +++ b/view_async.py @@ -0,0 +1,62 @@ +from __future__ import annotations +import asyncio +from LSP.event_loop import run_future +import sublime +import sublime_plugin + +open_view_futures_map: dict[str, asyncio.Future[sublime.View]] = {} +on_save_futures_map: dict[str, asyncio.Future] = {} + + +# todo rename +class AsyncViewListenerThingy(sublime_plugin.EventListener): + def on_load(self, view): + run_future(self.async_load(view)) + + def on_post_save(self, view): + run_future(self.async_on_save(view)) + + async def async_load(self, view: sublime.View): + file_name = view.file_name() or "" + if file_name not in open_view_futures_map: + return + future = open_view_futures_map.pop(file_name) + if future: + future.set_result(view) + + async def async_on_save(self, view: sublime.View): + file_name = view.file_name() or "" + if file_name not in on_save_futures_map: + return + future = on_save_futures_map.pop(file_name) + if future: + future.set_result(None) + + +async def save_view(view: sublime.View) -> None: + file_name = view.file_name() or "" + if not file_name: + view.run_command('save') + return + future: asyncio.Future = asyncio.Future() + on_save_futures_map[file_name] = future + view.run_command('save') + await future + + +async def open_view( + file_name, window: sublime.Window, + flags: sublime.NewFileFlags = sublime.NewFileFlags.NONE +) -> sublime.View: + future: asyncio.Future[sublime.View] = asyncio.Future() + open_view_futures_map[file_name] = future + active_view = window.active_view() + view = window.find_open_file(file_name) + if view: + future.set_result(view) + else: + window.open_file(file_name, flags=flags) + res = await future + if active_view: + window.focus_view(active_view) + return res From c6662f4c918b6549455e6f1a7dc0d8587d79c52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 10 Dec 2024 11:44:41 +0100 Subject: [PATCH 3/4] remove unneeded code --- delete_this_example.py | 27 ------------------ view_async.py | 62 ------------------------------------------ 2 files changed, 89 deletions(-) delete mode 100644 delete_this_example.py delete mode 100644 view_async.py diff --git a/delete_this_example.py b/delete_this_example.py deleted file mode 100644 index 69cdc84c8..000000000 --- a/delete_this_example.py +++ /dev/null @@ -1,27 +0,0 @@ -import sublime_plugin -from LSP.event_loop import run_future -from .view_async import open_view, save_view -from pathlib import Path - -class LspExampleAsyncCommand(sublime_plugin.TextCommand): - def run(self, edit): - print('1. this will be printed first') - run_future(self.open_edit_save_and_close_file()) - print('2. This will printed second') - - async def open_edit_save_and_close_file(self): - print('3. than this') - w = self.view.window() - if not w: - return - file_name = self.view.file_name() - if not file_name: - return - readme_file = str((Path(file_name) / Path('../README.md')).resolve()) - view = await open_view(readme_file, w) - view.run_command("append", { - 'characters': "LspExampleAsyncCommand added this" + '\n\n', - }) - await save_view(view) - # the view is saved at this point and safe to be closed - view.close() diff --git a/view_async.py b/view_async.py deleted file mode 100644 index 3c917bdc2..000000000 --- a/view_async.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations -import asyncio -from LSP.event_loop import run_future -import sublime -import sublime_plugin - -open_view_futures_map: dict[str, asyncio.Future[sublime.View]] = {} -on_save_futures_map: dict[str, asyncio.Future] = {} - - -# todo rename -class AsyncViewListenerThingy(sublime_plugin.EventListener): - def on_load(self, view): - run_future(self.async_load(view)) - - def on_post_save(self, view): - run_future(self.async_on_save(view)) - - async def async_load(self, view: sublime.View): - file_name = view.file_name() or "" - if file_name not in open_view_futures_map: - return - future = open_view_futures_map.pop(file_name) - if future: - future.set_result(view) - - async def async_on_save(self, view: sublime.View): - file_name = view.file_name() or "" - if file_name not in on_save_futures_map: - return - future = on_save_futures_map.pop(file_name) - if future: - future.set_result(None) - - -async def save_view(view: sublime.View) -> None: - file_name = view.file_name() or "" - if not file_name: - view.run_command('save') - return - future: asyncio.Future = asyncio.Future() - on_save_futures_map[file_name] = future - view.run_command('save') - await future - - -async def open_view( - file_name, window: sublime.Window, - flags: sublime.NewFileFlags = sublime.NewFileFlags.NONE -) -> sublime.View: - future: asyncio.Future[sublime.View] = asyncio.Future() - open_view_futures_map[file_name] = future - active_view = window.active_view() - view = window.find_open_file(file_name) - if view: - future.set_result(view) - else: - window.open_file(file_name, flags=flags) - res = await future - if active_view: - window.focus_view(active_view) - return res From 40180166351eb204093153e1e41f6fe6155884c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Tue, 10 Dec 2024 11:45:29 +0100 Subject: [PATCH 4/4] remove unused import --- event_loop/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event_loop/__init__.py b/event_loop/__init__.py index 616d4a3d3..6728c888f 100644 --- a/event_loop/__init__.py +++ b/event_loop/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio from threading import Thread -from typing import Any, Awaitable +from typing import Awaitable from ..plugin.core.logging import debug __loop: asyncio.AbstractEventLoop | None = None