Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add event loop #2576

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the event loop starts, when the LSP module is loaded,
and the loop ends when ST exits.

27 changes: 27 additions & 0 deletions delete_this_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import sublime_plugin
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file will be deleted.

from LSP.event_loop import run_future
from .view_async import open_view, save_view
from pathlib import Path

class LspExampleAsyncCommand(sublime_plugin.TextCommand):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want to try this command, here is a keybinding {"keys": ["primary+i"], "command": "lsp_example_async" } or view.run_command('lsp_example_async') if you want to run it from the ST console.

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()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These few lines illustrate one of the benefits.
On line 22, the view is guaranteed to be loaded.
After line 25, the view is guaranteed to be saved.

54 changes: 54 additions & 0 deletions event_loop/__init__.py
Original file line number Diff line number Diff line change
@@ -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")
62 changes: 62 additions & 0 deletions view_async.py
Original file line number Diff line number Diff line change
@@ -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
Loading