diff --git a/plugin.py b/plugin.py index 513696e..f5ca185 100644 --- a/plugin.py +++ b/plugin.py @@ -1,75 +1,47 @@ import sublime import sublime_plugin -# import Codemp.codemp_client as codemp -from Codemp.src.codemp_client import ( - VirtualClient, - status_log, - safe_listener_detach, - is_active, -)xs +from Codemp.src.codemp_client import VirtualClient +from Codemp.src.TaskManager import rt +from Codemp.src.utils import status_log, is_active, safe_listener_detach +import Codemp.src.globals as g -# UGLYYYY, find a way to not have global variables laying around. -_client = None -_txt_change_listener = None - -_palette = [ - "var(--redish)", - "var(--orangish)", - "var(--yellowish)", - "var(--greenish)", - "var(--cyanish)", - "var(--bluish)", - "var(--purplish)", - "var(--pinkish)", -] - -_regions_colors = [ - "region.redish", - "region.orangeish", - "region.yellowish", - "region.greenish", - "region.cyanish", - "region.bluish", - "region.purplish", - "region.pinkish", -] +CLIENT = None +TEXT_LISTENER = None # Initialisation and Deinitialisation ############################################################################## - - def plugin_loaded(): - global _client - global _txt_change_listener + global CLIENT + global TEXT_LISTENER # instantiate and start a global asyncio event loop. # pass in the exit_handler coroutine that will be called upon relasing the event loop. - _client = VirtualClient(disconnect_client) - _txt_change_listener = CodempClientTextChangeListener() + CLIENT = VirtualClient(disconnect_client) + TEXT_LISTENER = CodempClientTextChangeListener() status_log("plugin loaded") async def disconnect_client(): - global _client - global _txt_change_listener + global CLIENT + global TEXT_LISTENER - safe_listener_detach(_txt_change_listener) - _client.tm.stop_all() + safe_listener_detach(TEXT_LISTENER) + CLIENT.tm.stop_all() - for vws in _client.workspaces: + for vws in CLIENT.workspaces.values(): vws.cleanup() - # fime: allow riconnections - _client = None + # fix me: allow riconnections + CLIENT = None def plugin_unloaded(): - global _client + global CLIENT # releasing the runtime, runs the disconnect callback defined when acquiring the event loop. - _client.tm.release(False) + CLIENT.tm.release(False) status_log("plugin unloaded") @@ -101,80 +73,52 @@ def get_view_from_local_path(path): return view -def cleanup_tags(view): - del view.settings()["codemp_buffer"] - view.erase_status("z_codemp_buffer") - view.erase_regions("codemp_cursors") - - -def tag(view): - view.set_status("z_codemp_buffer", "[Codemp]") - view.settings()["codemp_buffer"] = True - -# The main workflow: -# Plugin loads and initialises an empty handle to the client -# The plugin calls connect and populates the handle with a client instance -# We use the client to authenticate and login (to a workspace) to obtain a token -# We join a workspace (either new or existing) - - # Listeners ############################################################################## +class EventListener(sublime_plugin.EventListener): + def on_exit(self) -> None: + global CLIENT + CLIENT.tm.release(True) class CodempClientViewEventListener(sublime_plugin.ViewEventListener): @classmethod def is_applicable(cls, settings): - return settings.get("codemp_buffer", False) + return settings.get(g.CODEMP_BUFFER_VIEW_TAG, False) @classmethod def applies_to_primary_view_only(cls): return False def on_selection_modified_async(self): - global _client - vbuff = _client.active_workspace.get_virtual_by_local(self.view.buffer_id()) + global CLIENT + vbuff = CLIENT.active_workspace.get_by_local(self.view.buffer_id()) if vbuff is not None: - _client.send_cursor(vbuff) + CLIENT.send_cursor(vbuff) # We only edit on one view at a time, therefore we only need one TextChangeListener # Each time we focus a view to write on it, we first attach the listener to that buffer. # When we defocus, we detach it. def on_activated(self): - global _txt_change_listener + global TEXT_LISTENER print("view {} activated".format(self.view.id())) - _txt_change_listener.attach(self.view.buffer()) + TEXT_LISTENER.attach(self.view.buffer()) def on_deactivated(self): - global _txt_change_listener + global TEXT_LISTENER print("view {} deactivated".format(self.view.id())) - safe_listener_detach(_txt_change_listener) - - def on_text_command(self, command_name, args): - print(self.view.id(), command_name, args) - if command_name == "codemp_replace_text": - print("dry_run: detach text listener") - - def on_post_text_command(self, command_name, args): - print(command_name, args) - if command_name == "codemp_replace_text": - print("dry_run: attach text listener") - - # UPDATE ME + safe_listener_detach(TEXT_LISTENER) def on_pre_close(self): - global _client - global _txt_change_listener + global TEXT_LISTENER if is_active(self.view): - safe_listener_detach(_txt_change_listener) + safe_listener_detach(TEXT_LISTENER) - vbuff = _client.active_workspace.get_virtual_by_local(self.view.buffer_id()) + global CLIENT + vbuff = CLIENT.active_workspace.get_by_local(self.view.buffer_id()) vbuff.cleanup() - print(list(map(lambda x: x.get_name(), _client.tm.tasks))) - task = _client.tm.cancel_and_pop(f"buffer-ctl-{vbuff.codemp_id}") - print(list(map(lambda x: x.get_name(), _client.tm.tasks))) - print(task.cancelled()) + CLIENT.tm.stop_and_pop(f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}") # have to run the detach logic in sync, to keep a valid reference to the view. # sublime_asyncio.sync(buffer.detach(_client)) @@ -186,20 +130,20 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): # we'll do it by hand with .attach(buffer). return False - # lets make this blocking :D - # def on_text_changed_async(self, changes): + # blocking :D def on_text_changed(self, changes): - global _client if ( self.buffer.primary_view() .settings() - .get("codemp_ignore_next_on_modified_text_event", None) + .get(g.CODEMP_IGNORE_NEXT_TEXT_CHANGE, None) ): status_log("ignoring echoing back the change.") - self.view.settings()["codemp_ignore_next_on_modified_text_event"] = False + self.view.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False return - vbuff = _client.active_workspace.get_virtual_by_local(self.buffer.id()) - _client.send_buffer_change(changes, vbuff) + + global CLIENT + vbuff = CLIENT.active_workspace.get_by_local(self.buffer.id()) + CLIENT.send_buffer_change(changes, vbuff) # Commands: @@ -214,8 +158,8 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): ############################################################################# class CodempConnectCommand(sublime_plugin.WindowCommand): def run(self, server_host): - global _client - sublime_asyncio.dispatch(_client.connect(server_host)) + global CLIENT + rt.dispatch(CLIENT.connect(server_host)) def input(self, args): if "server_host" not in args: @@ -234,8 +178,8 @@ class ServerHostInputHandler(sublime_plugin.TextInputHandler): ############################################################################# class CodempJoinCommand(sublime_plugin.WindowCommand): def run(self, workspace_id): - global _client - sublime_asyncio.dispatch(_client.join_workspace(workspace_id)) + global CLIENT + rt.dispatch(CLIENT.join_workspace(workspace_id)) def input_description(self): return "Join Workspace:" @@ -254,9 +198,9 @@ class WorkspaceIdInputHandler(sublime_plugin.TextInputHandler): ############################################################################# class CodempAttachCommand(sublime_plugin.WindowCommand): def run(self, buffer_id): - global _client - if _client.active_workspace is not None: - sublime_asyncio.dispatch(_client.active_workspace.attach(buffer_id)) + global CLIENT + if CLIENT.active_workspace is not None: + rt.dispatch(CLIENT.active_workspace.attach(buffer_id)) else: sublime.error_message( "You haven't joined any worksapce yet. use `Codemp: Join Workspace`" @@ -267,10 +211,10 @@ class CodempAttachCommand(sublime_plugin.WindowCommand): # This is awful, fix it def input(self, args): - global _client - if _client.active_workspace is not None: + global CLIENT + if CLIENT.active_workspace is not None: if "buffer_id" not in args: - existing_buffers = _client.active_workspace.handle.filetree() + existing_buffers = CLIENT.active_workspace.handle.filetree() if len(existing_buffers) == 0: return BufferIdInputHandler() else: @@ -284,7 +228,7 @@ class CodempAttachCommand(sublime_plugin.WindowCommand): class BufferIdInputHandler(sublime_plugin.TextInputHandler): def initial_text(self): - return "Create New Buffer:" + return "No buffers found in the workspace. Create new: " class ListBufferIdInputHandler(sublime_plugin.ListInputHandler): @@ -292,8 +236,8 @@ class ListBufferIdInputHandler(sublime_plugin.ListInputHandler): return "buffer_id" def list_items(self): - global _client - return _client.active_workspace.handle.filetree() + global CLIENT + return CLIENT.active_workspace.handle.filetree() def next_input(self, args): if "buffer_id" not in args: @@ -350,7 +294,7 @@ class CodempReplaceTextCommand(sublime_plugin.TextCommand): ############################################################################# class CodempDisconnectCommand(sublime_plugin.WindowCommand): def run(self): - sublime_asyncio.sync(disconnect_client()) + rt.sync(disconnect_client()) # Proxy Commands ( NOT USED ) diff --git a/src/TaskManager.py b/src/TaskManager.py new file mode 100644 index 0000000..43588a1 --- /dev/null +++ b/src/TaskManager.py @@ -0,0 +1,63 @@ +from typing import Optional +import Codemp.ext.sublime_asyncio as rt + + +class TaskManager: + def __init__(self, exit_handler): + self.tasks = [] + self.exit_handler_id = rt.acquire(exit_handler) + + def release(self, at_exit): + rt.release(at_exit=at_exit, exit_handler_id=self.exit_handler_id) + + def dispatch(self, coro, name): + rt.dispatch(coro, self.store_named_lambda(name)) + + def sync(self, coro): + rt.sync(coro) + + def store(self, task): + self.tasks.append(task) + + def store_named(self, task, name=None): + task.set_name(name) + self.store(task) + + def store_named_lambda(self, name): + def _store(task): + task.set_name(name) + self.store(task) + + return _store + + def get_task(self, name) -> Optional: + return next((t for t in self.tasks if t.get_name() == name), None) + + def get_task_idx(self, name) -> Optional: + return next( + (i for (i, t) in enumerate(self.tasks) if t.get_name() == name), None + ) + + def pop_task(self, name) -> Optional: + idx = self.get_task_idx(name) + if id is not None: + return self.task.pop(idx) + return None + + def stop(self, name): + t = self.get_task(name) + if t is not None: + t.cancel() + + def stop_and_pop(self, name) -> Optional: + idx, task = next( + ((i, t) for (i, t) in enumerate(self.tasks) if t.get_name() == name), + (None, None), + ) + if idx is not None: + task.cancel() + return self.tasks.pop(idx) + + def stop_all(self): + for task in self.tasks: + task.cancel() diff --git a/src/codemp_client.py b/src/codemp_client.py index 75ce487..2938f8f 100644 --- a/src/codemp_client.py +++ b/src/codemp_client.py @@ -1,43 +1,18 @@ from __future__ import annotations from typing import Optional, Callable + import sublime -import sublime_plugin - -import Codemp.ext.sublime_asyncio as sublime_asyncio - import asyncio # noqa: F401 import typing # noqa: F401 import tempfile import os -from Codemp.bindings.codemp_client import codemp_init, PyCursorEvent, PyTextChange, PyId - -# Some utility functions -def status_log(msg): - sublime.status_message("[codemp] {}".format(msg)) - print("[codemp] {}".format(msg)) - - -def rowcol_to_region(view, start, end): - a = view.text_point(start[0], start[1]) - b = view.text_point(end[0], end[1]) - return sublime.Region(a, b) - - -def is_active(view): - if view.window().active_view() == view: - return True - return False - - -def safe_listener_detach(txt_listener): - if txt_listener.is_attached(): - txt_listener.detach() - - -############################################################################### +import Codemp.src.globals as g +from Codemp.src.wrappers import BufferController, Workspace, Client +from Codemp.src.utils import status_log, is_active, rowcol_to_region +from Codemp.src.TaskManager import TaskManager # This class is used as an abstraction between the local buffers (sublime side) and the @@ -47,15 +22,14 @@ def safe_listener_detach(txt_listener): class VirtualBuffer: def __init__( self, - view: sublime.View, - remote_id: str, workspace: VirtualWorkspace, + remote_id: str, + view: sublime.View, buffctl: BufferController, ): self.view = view self.codemp_id = remote_id self.sublime_id = view.buffer_id() - self.worker_task_name = "buffer-worker-{}".format(self.codemp_id) self.workspace = workspace self.buffctl = buffctl @@ -68,29 +42,21 @@ class VirtualBuffer: self.view.set_scratch(True) # mark the view as a codemp view - self.view.set_status("z_codemp_buffer", "[Codemp]") - self.view.settings()["codemp_buffer"] = True - - # # start the buffer worker that waits for text_changes in the worker thread - # sublime_asyncio.dispatch( - # self.apply_buffer_change_task(), store_task(self.worker_task_name) - # ) + self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]") + self.view.settings()[g.CODEMP_BUFFER_VIEW_TAG] = True def cleanup(self): os.remove(self.tmpfile) # cleanup views - del self.view.settings()["codemp_buffer"] - self.view.erase_status("z_codemp_buffer") - self.view.erase_regions("codemp_cursors") - - # the text listener should be detached by the event listener - # on close and on_deactivated events. + del self.view.settings()[g.CODEMP_BUFFER_VIEW_TAG] + self.view.erase_status(g.SUBLIME_STATUS_ID) + # this does nothing for now. figure out a way later + # self.view.erase_regions(g.SUBLIME_REGIONS_PREFIX) + status_log(f"cleaning up virtual buffer '{self.codemp_id}'") # A virtual workspace is a bridge class that aims to translate # events that happen to the codemp workspaces into sublime actions - - class VirtualWorkspace: def __init__(self, client: VirtualClient, workspace_id: str, handle: Workspace): self.id = workspace_id @@ -99,9 +65,10 @@ class VirtualWorkspace: self.handle = handle self.curctl = handle.cursor() - self.active_buffers: list[VirtualBuffer] = [] + # mapping local buffer ids -> remote ids + self.id_map: dict[str, str] = {} + self.active_buffers: dict[str, VirtualBuffer] = {} - # REMEMBER TO DELETE THE TEMP STUFF! # initialise the virtual filesystem tmpdir = tempfile.mkdtemp(prefix="codemp_") status_log("setting up virtual fs for workspace in: {} ".format(tmpdir)) @@ -111,35 +78,39 @@ class VirtualWorkspace: proj_data = self.sublime_window.project_data() if proj_data is None: proj_data = {"folders": []} + proj_data["folders"].append( - {"name": "CODEMP::" + workspace_id, "path": self.rootdir} + {"name": f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}", "path": self.rootdir} ) self.sublime_window.set_project_data(proj_data) - # start the event listener? + def add_buffer(self, remote_id: str, vbuff: VirtualBuffer): + self.id_map[vbuff.view.buffer_id()] = remote_id + self.active_buffers[remote_id] = vbuff def cleanup(self): # the worskpace only cares about closing the various open views on its buffers. # the event listener calls the cleanup code for each buffer independently on its own. - for vbuff in self.active_buffers: + for vbuff in self.active_buffers.values(): vbuff.view.close() d = self.sublime_window.project_data() - newf = filter(lambda F: not F["name"].startwith("CODEMP::"), d["folders"]) + newf = list( + filter( + lambda F: F["name"] != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}", + d["folders"], + ) + ) d["folders"] = newf self.sublime_window.set_project_data(d) - + status_log(f"cleaning up virtual workspace '{self.id}'") os.removedirs(self.rootdir) - def get_virtual_by_local(self, id: str) -> Optional[VirtualBuffer]: - return next( - (vbuff for vbuff in self.active_buffers if vbuff.sublime_id == id), None - ) + def get_by_local(self, local_id: str) -> Optional[VirtualBuffer]: + return self.active_buffers.get(self.id_map.get(local_id)) - def get_virtual_by_remote(self, id: str) -> Optional[VirtualBuffer]: - return next( - (vbuff for vbuff in self.active_buffers if vbuff.codemp_id == id), None - ) + def get_by_remote(self, remote_id: str) -> Optional[VirtualBuffer]: + return self.active_buffers.get(remote_id) async def attach(self, id: str): if id is None: @@ -161,56 +132,31 @@ class VirtualWorkspace: status_log(f"error when attaching to buffer '{id}': {e}") return - # REMEMBER TO DEAL WITH DELETING THESE THINGS! view = self.sublime_window.new_file() - vbuff = VirtualBuffer(view, id, self, buff_ctl) - self.active_buffers.append(vbuff) + vbuff = VirtualBuffer(self, id, view, buff_ctl) + self.add_buffer(id, vbuff) self.client.spawn_buffer_manager(vbuff) - # if the view is already active calling focus_view() will not trigger the on_activate() + # TODO! if the view is already active calling focus_view() will not trigger the on_activate self.sublime_window.focus_view(view) class VirtualClient: def __init__(self, on_exit: Callable = None): self.handle: Client = Client() - self.workspaces: list[VirtualWorkspace] = [] + self.workspaces: dict[str, VirtualWorkspace] = {} self.active_workspace: VirtualWorkspace = None self.tm = TaskManager(on_exit) - self.change_clock = 0 - def make_active(self, ws: VirtualWorkspace): # TODO: Logic to deal with swapping to and from workspaces, # what happens to the cursor tasks etc.. if self.active_workspace is not None: - self.tm.stop_and_pop(f"move-cursor-{self.active_workspace.id}") + self.tm.stop_and_pop(f"{g.CURCTL_TASK_PREFIX}-{self.active_workspace.id}") self.active_workspace = ws self.spawn_cursor_manager(ws) - def get_virtual_local(self, id: str) -> Optional[VirtualWorkspace]: - # get's the workspace that contains a buffer - next( - ( - vws - for vws in self.workspaces - if vws.get_virtual_by_local(id) is not None - ), - None, - ) - - def get_virtual_remote(self, id: str) -> Optional[VirtualWorkspace]: - # get's the workspace that contains a buffer - next( - ( - vws - for vws in self.workspaces - if vws.get_virtual_by_remote(id) is not None - ), - None, - ) - async def connect(self, server_host: str): status_log(f"Connecting to {server_host}") try: @@ -239,29 +185,22 @@ class VirtualClient: sublime.error_message(f"Could not join workspace '{workspace_id}': {e}") return - print(workspace_handle.id()) - - # here we should also start the workspace event watcher task vws = VirtualWorkspace(self, workspace_id, workspace_handle) self.make_active(vws) - self.workspaces.append(vws) + + self.workspaces[workspace_id] = vws def spawn_cursor_manager(self, virtual_workspace: VirtualWorkspace): async def move_cursor_task(vws): - global _regions_colors - global _palette - status_log(f"spinning up cursor worker for workspace '{vws.id}'...") - # TODO: make the matching user/color more solid. now all users have one color cursor. - # Maybe make all cursors the same color and only use annotations as a discriminant. - # idea: use a user id hash map that maps to a color. try: while cursor_event := await vws.curctl.recv(): - vbuff = vws.get_virtual_by_remote(cursor_event.buffer) + vbuff = vws.get_by_remote(cursor_event.buffer) if vbuff is None: status_log( - f"Received a cursor event for an unknown or inactive buffer: {cursor_event.buffer}" + f"Received a cursor event for an unknown \ + or inactive buffer: {cursor_event.buffer}" ) continue @@ -271,14 +210,13 @@ class VirtualClient: reg_flags = sublime.RegionFlags.DRAW_EMPTY # show cursors. user_hash = hash(cursor_event.user) - vbuff.view.add_regions( - f"codemp-cursors-{user_hash}", + f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}", [reg], flags=reg_flags, - scope=_regions_colors[user_hash % len(_regions_colors)], + scope=g.REGIONS_COLORS[user_hash % len(g.REGIONS_COLORS)], annotations=[cursor_event.user], - annotation_color=_palette[user_hash % len(_palette)], + annotation_color=g.PALETTE[user_hash % len(g.PALETTE)], ) except asyncio.CancelledError: @@ -286,7 +224,8 @@ class VirtualClient: return self.tm.dispatch( - move_cursor_task(virtual_workspace), f"cursor-ctl-{virtual_workspace.id}" + move_cursor_task(virtual_workspace), + f"{g.CURCTL_TASK_PREFIX}-{virtual_workspace.id}", ) def send_cursor(self, vbuff: VirtualBuffer): @@ -299,22 +238,18 @@ class VirtualClient: vbuff.workspace.curctl.send(vbuff.codemp_id, start, end) def spawn_buffer_manager(self, vbuff: VirtualBuffer): - status_log("spawning buffer manager") - async def apply_buffer_change_task(vb): status_log(f"spinning up '{vb.codemp_id}' buffer worker...") try: while text_change := await vb.buffctl.recv(): - # In case a change arrives to a background buffer, just apply it. - # We are not listening on it. Otherwise, interrupt the listening - # to avoid echoing back the change just received. if text_change.is_empty(): status_log("change is empty. skipping.") continue - - vb.view.settings()[ - "codemp_ignore_next_on_modified_text_event" - ] = True + # In case a change arrives to a background buffer, just apply it. + # We are not listening on it. Otherwise, interrupt the listening + # to avoid echoing back the change just received. + if is_active(vb.view): + vb.view.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True # we need to go through a sublime text command, since the method, # view.replace needs an edit token, that is obtained only when calling @@ -333,7 +268,8 @@ class VirtualClient: status_log("'{}' buffer worker stopped...".format(vb.codemp_id)) self.tm.dispatch( - apply_buffer_change_task(vbuff), f"buffer-ctl-{vbuff.codemp_id}" + apply_buffer_change_task(vbuff), + f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}", ) def send_buffer_change(self, changes, vbuff: VirtualBuffer): @@ -347,162 +283,3 @@ class VirtualClient: ) ) vbuff.buffctl.send(region.begin(), region.end(), change.str) - - -class TaskManager: - def __init__(self, exit_handler): - self.tasks = [] - self.exit_handler_id = sublime_asyncio.acquire(exit_handler) - - def release(self, at_exit): - sublime_asyncio.release(at_exit, self.exit_handler_id) - - def dispatch(self, coro, name): - sublime_asyncio.dispatch(coro, self.store_named_lambda(name)) - - def sync(self, coro): - sublime_asyncio.sync(coro) - - def store(self, task): - self.tasks.append(task) - - def store_named(self, task, name=None): - task.set_name(name) - self.store(task) - - def store_named_lambda(self, name): - def _store(task): - task.set_name(name) - self.store(task) - - return _store - - def get_task(self, name) -> Optional: - return next((t for t in self.tasks if t.get_name() == name), None) - - def get_task_idx(self, name) -> Optional: - return next( - (i for (i, t) in enumerate(self.tasks) if t.get_name() == name), None - ) - - def pop_task(self, name) -> Optional: - idx = self.get_task_idx(name) - if id is not None: - return self.task.pop(idx) - return None - - def stop(self, name): - t = self.get_task(name) - if t is not None: - t.cancel() - - def stop_and_pop(self, name) -> Optional: - idx, task = next( - ((i, t) for (i, t) in enumerate(self.tasks) if t.get_name() == name), - (None, None), - ) - if idx is not None: - task.cancel() - return self.tasks.pop(idx) - - def stop_all(self): - for task in self.tasks: - task.cancel() - -###################################################################################### -# These are helper wrappers, that wrap the coroutines returned from the -# pyo3 bindings into usable awaitable functions. -# These should not be directly extended but rather use the higher level "virtual" counterparts above. - -# All methods, without an explicit 'noexcept' are to be treated as failable -# and can throw an error - - -class CursorController: - def __init__(self, handle) -> None: # noexcept - self.handle = handle - - def send(self, path: str, start: tuple[int, int], end: tuple[int, int]) -> None: - self.handle.send(path, start, end) - - def try_recv(self) -> Optional[PyCursorEvent]: - return self.handle.try_recv() - - async def recv(self) -> PyCursorEvent: - return await self.handle.recv() - - async def poll(self) -> None: - # await until new cursor event, then returns - return await self.handle.poll() - - -class BufferController: - def __init__(self, handle) -> None: # noexcept - self.handle = handle - - def send(self, start: int, end: int, txt: str) -> None: - self.handle.send(start, end, txt) - - def try_recv(self) -> Optional[PyTextChange]: - return self.handle.try_recv() - - async def recv(self) -> PyTextChange: - return await self.handle.recv() - - async def poll(self) -> None: - return await self.handle.poll() - - -class Workspace: - def __init__(self, handle) -> None: # noexcept - self.handle = handle - - async def create(self, path: str) -> None: - await self.handle.create(path) - - async def attach(self, path: str) -> BufferController: - return BufferController(await self.handle.attach(path)) - - async def fetch_buffers(self) -> None: - await self.handle.fetch_buffers() - - async def fetch_users(self) -> None: - await self.handle.fetch_users() - - async def list_buffer_users(self, path: str) -> list[PyId]: - return await self.handle.list_buffer_users(path) - - async def delete(self, path) -> None: - await self.handle.delete(path) - - def id(self) -> str: # noexcept - return self.handle.id() - - def cursor(self) -> CursorController: - return CursorController(self.handle.cursor()) - - def buffer_by_name(self, path) -> BufferController: - return BufferController(self.handle.buffer_by_name(path)) - - def filetree(self) -> list[str]: # noexcept - return self.handle.filetree() - - -class Client: - def __init__(self) -> None: - self.handle = codemp_init() - - async def connect(self, server_host: str) -> None: - await self.handle.connect(server_host) - - async def login(self, user: str, password: str, workspace: Optional[str]) -> None: - await self.handle.login(user, password, workspace) - - async def join_workspace(self, workspace: str) -> Workspace: - return Workspace(await self.handle.join_workspace(workspace)) - - async def get_workspace(self, id: str) -> Optional[Workspace]: - return Workspace(await self.handle.get_workspace(id)) - - async def user_id(self) -> str: - return await self.handle.user_id() diff --git a/src/globals.py b/src/globals.py new file mode 100644 index 0000000..b258d63 --- /dev/null +++ b/src/globals.py @@ -0,0 +1,29 @@ +BUFFCTL_TASK_PREFIX = "buffer-ctl" +CURCTL_TASK_PREFIX = "cursor-ctl" +WORKSPACE_FOLDER_PREFIX = "CODEMP::" +SUBLIME_REGIONS_PREFIX = "codemp-cursors" +CODEMP_BUFFER_VIEW_TAG = "codemp-buffer" +SUBLIME_STATUS_ID = "z_codemp_buffer" +CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event" + +PALETTE = [ + "var(--redish)", + "var(--orangish)", + "var(--yellowish)", + "var(--greenish)", + "var(--cyanish)", + "var(--bluish)", + "var(--purplish)", + "var(--pinkish)", +] + +REGIONS_COLORS = [ + "region.redish", + "region.orangeish", + "region.yellowish", + "region.greenish", + "region.cyanish", + "region.bluish", + "region.purplish", + "region.pinkish", +] diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..8f3751a --- /dev/null +++ b/src/utils.py @@ -0,0 +1,24 @@ +import sublime +import sublime_plugin + + +def status_log(msg): + sublime.status_message("[codemp] {}".format(msg)) + print("[codemp] {}".format(msg)) + + +def rowcol_to_region(view, start, end): + a = view.text_point(start[0], start[1]) + b = view.text_point(end[0], end[1]) + return sublime.Region(a, b) + + +def is_active(view): + if view.window().active_view() == view: + return True + return False + + +def safe_listener_detach(txt_listener: sublime_plugin.TextChangeListener): + if txt_listener.is_attached(): + txt_listener.detach() diff --git a/src/wrappers.py b/src/wrappers.py new file mode 100644 index 0000000..0964662 --- /dev/null +++ b/src/wrappers.py @@ -0,0 +1,101 @@ +from __future__ import annotations +from typing import Optional +from Codemp.bindings.codemp_client import codemp_init, PyCursorEvent, PyTextChange, PyId + +###################################################################################### +# These are helper wrappers, that wrap the coroutines returned from the +# pyo3 bindings into usable awaitable functions. +# These should not be directly extended but rather use the higher +# level "virtual" counterparts above. + +# All methods, without an explicit 'noexcept' are to be treated as failable +# and can throw an error + + +class CursorController: + def __init__(self, handle) -> None: # noexcept + self.handle = handle + + def send(self, path: str, start: tuple[int, int], end: tuple[int, int]) -> None: + self.handle.send(path, start, end) + + def try_recv(self) -> Optional[PyCursorEvent]: + return self.handle.try_recv() + + async def recv(self) -> PyCursorEvent: + return await self.handle.recv() + + async def poll(self) -> None: + return await self.handle.poll() + + +class BufferController: + def __init__(self, handle) -> None: # noexcept + self.handle = handle + + def send(self, start: int, end: int, txt: str) -> None: + self.handle.send(start, end, txt) + + def try_recv(self) -> Optional[PyTextChange]: + return self.handle.try_recv() + + async def recv(self) -> PyTextChange: + return await self.handle.recv() + + async def poll(self) -> None: + return await self.handle.poll() + + +class Workspace: + def __init__(self, handle) -> None: # noexcept + self.handle = handle + + async def create(self, path: str) -> None: + await self.handle.create(path) + + async def attach(self, path: str) -> BufferController: + return BufferController(await self.handle.attach(path)) + + async def fetch_buffers(self) -> None: + await self.handle.fetch_buffers() + + async def fetch_users(self) -> None: + await self.handle.fetch_users() + + async def list_buffer_users(self, path: str) -> list[PyId]: + return await self.handle.list_buffer_users(path) + + async def delete(self, path) -> None: + await self.handle.delete(path) + + def id(self) -> str: # noexcept + return self.handle.id() + + def cursor(self) -> CursorController: + return CursorController(self.handle.cursor()) + + def buffer_by_name(self, path) -> BufferController: + return BufferController(self.handle.buffer_by_name(path)) + + def filetree(self) -> list[str]: # noexcept + return self.handle.filetree() + + +class Client: + def __init__(self) -> None: + self.handle = codemp_init() + + async def connect(self, server_host: str) -> None: + await self.handle.connect(server_host) + + async def login(self, user: str, password: str, workspace: Optional[str]) -> None: + await self.handle.login(user, password, workspace) + + async def join_workspace(self, workspace: str) -> Workspace: + return Workspace(await self.handle.join_workspace(workspace)) + + async def get_workspace(self, id: str) -> Optional[Workspace]: + return Workspace(await self.handle.get_workspace(id)) + + async def user_id(self) -> str: + return await self.handle.user_id()