From 744887b33683f0cab7e60a88a252df743ae508bf Mon Sep 17 00:00:00 2001 From: Camillo Schenone Date: Sun, 4 Aug 2024 19:57:59 +0200 Subject: [PATCH] feat: major refactor. the client doesn't hold the task manager anymore, instead it is now a singleton that all objects dump their tasks into to be managed. This allows to better separate the responsibilities of the various virtual-objects. The client now is also a module-singleton which allows us to remove that ugly global variable. fixup: uses relative imports instead that absolute ones with Codemp in front. rename of codemp_client.py into just client.py Former-commit-id: 4a0cc20e82f9631931ba2f61379a61c461b1f291 --- plugin.py | 115 ++++++----- src/TaskManager.py | 34 +++- src/{codemp_client.py => client.py} | 300 ++++++++++++++++------------ src/globals.py | 2 + src/utils.py | 7 +- 5 files changed, 260 insertions(+), 198 deletions(-) rename src/{codemp_client.py => client.py} (56%) diff --git a/plugin.py b/plugin.py index aed653f..b736c71 100644 --- a/plugin.py +++ b/plugin.py @@ -5,71 +5,80 @@ import sublime_plugin # import sys # import importlib.util -from .src.codemp_client import VirtualClient -from .src.codemp_client import CodempLogger -from .src.TaskManager import rt +from .src.TaskManager import tm +from .src.client import client, VirtualClient +from .src.client import CodempLogger from .src.utils import status_log from .src.utils import safe_listener_detach +from .src.utils import safe_listener_attach from .src import globals as g from .bindings.codemp import init_logger -CLIENT = None TEXT_LISTENER = None - # Initialisation and Deinitialisation ############################################################################## + + def plugin_loaded(): - 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) - TEXT_LISTENER = CodempClientTextChangeListener() + tm.acquire(disconnect_client) logger = CodempLogger(init_logger(False)) - CLIENT.tm.dispatch(logger.spawn_logger(), "codemp-logger") + tm.dispatch(logger.log(), "codemp-logger") + + TEXT_LISTENER = CodempClientTextChangeListener() status_log("plugin loaded") async def disconnect_client(): - global CLIENT global TEXT_LISTENER - safe_listener_detach(TEXT_LISTENER) - CLIENT.tm.stop_all() + tm.stop_all() - for vws in CLIENT.workspaces.values(): + if TEXT_LISTENER is not None: + safe_listener_detach(TEXT_LISTENER) + + for vws in client.workspaces.values(): vws.cleanup() def plugin_unloaded(): - global CLIENT # releasing the runtime, runs the disconnect callback defined when acquiring the event loop. - CLIENT.tm.release(False) + status_log("unloading") + tm.release(False) # Listeners ############################################################################## class EventListener(sublime_plugin.EventListener): def on_exit(self): - global CLIENT - CLIENT.tm.release(True) + tm.release(True) def on_pre_close_window(self, window): - global CLIENT + if client.active_workspace is None: + return # nothing to do + + client.make_active(None) + s = window.settings() - if s.get(g.CODEMP_WINDOW_TAG, False): - for wsid in s[g.CODEMP_WINDOW_WORKSPACES]: - ws = CLIENT[wsid] - if ws is not None: - if ws.id == CLIENT.active_workspace.id: - CLIENT.active_workspace = None - CLIENT.tm.stop(f"{g.CURCTL_TASK_PREFIX}-{ws.id}") - ws.cleanup() - del CLIENT.workspaces[wsid] + if not s.get(g.CODEMP_WINDOW_TAG, False): + return + + for wsid in s[g.CODEMP_WINDOW_WORKSPACES]: + ws = client[wsid] + if ws is None: + status_log( + "[WARN] a tag on the window was found but not a matching workspace." + ) + continue + + ws.cleanup() + del client.workspaces[wsid] class CodempClientViewEventListener(sublime_plugin.ViewEventListener): @@ -82,12 +91,13 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): return False def on_selection_modified_async(self): - s = self.view.settings() + ws = client.get_workspace(self.view) + if ws is None: + return - global CLIENT - vbuff = CLIENT[s[g.CODEMP_WORKSPACE_ID]].get_by_local(self.view.buffer_id()) + vbuff = ws.get_by_local(self.view.buffer_id()) if vbuff is not None: - CLIENT.send_cursor(vbuff) + vbuff.send_cursor(ws) def on_activated(self): # sublime has no proper way to check if a view gained or lost input focus outside of this @@ -95,7 +105,7 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): g.ACTIVE_CODEMP_VIEW = self.view.id() # print("view {} activated".format(self.view.id())) global TEXT_LISTENER - TEXT_LISTENER.attach(self.view.buffer()) + safe_listener_attach(TEXT_LISTENER, self.view.buffer()) def on_deactivated(self): g.ACTIVE_CODEMP_VIEW = None @@ -108,12 +118,13 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): if self.view.id() == g.ACTIVE_CODEMP_VIEW: safe_listener_detach(TEXT_LISTENER) - global CLIENT - wsid = self.view.settings().get(g.CODEMP_WORKSPACE_ID) - vbuff = CLIENT[wsid].get_by_local(self.view.buffer_id()) - vbuff.cleanup() + ws = client.get_workspace(self.view) + if ws is None: + return - CLIENT.tm.stop(f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}") + vbuff = ws.get_by_local(self.view.buffer_id()) + if vbuff is not None: + vbuff.cleanup() class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): @@ -131,9 +142,9 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False return - global CLIENT - vbuff = CLIENT[s[g.CODEMP_WORKSPACE_ID]].get_by_local(self.buffer.id()) - CLIENT.send_buffer_change(changes, vbuff) + vbuff = client.get_buffer(self.buffer.primary_view()) + if vbuff is not None: + vbuff.send_buffer_change(changes) # Commands: @@ -153,8 +164,7 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): ############################################################################# class CodempConnectCommand(sublime_plugin.WindowCommand): def run(self, server_host): - global CLIENT - rt.dispatch(CLIENT.connect(server_host)) + tm.dispatch(client.connect(server_host)) def input(self, args): if "server_host" not in args: @@ -174,8 +184,7 @@ async def JoinCommand(client: VirtualClient, workspace_id: str, buffer_id: str): class CodempJoinCommand(sublime_plugin.WindowCommand): def run(self, workspace_id, buffer_id): - global CLIENT - rt.dispatch(JoinCommand(CLIENT, workspace_id, buffer_id)) + tm.dispatch(JoinCommand(client, workspace_id, buffer_id)) def input_description(self): return "Join:" @@ -189,8 +198,7 @@ class CodempJoinCommand(sublime_plugin.WindowCommand): ############################################################################# class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand): def run(self, workspace_id): - global CLIENT - rt.dispatch(CLIENT.join_workspace(workspace_id, "sublime3")) + tm.dispatch(client.join_workspace(workspace_id, "sublime3")) def input_description(self): return "Join specific workspace" @@ -204,23 +212,21 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand): ############################################################################# class CodempJoinBufferCommand(sublime_plugin.WindowCommand): def run(self, buffer_id): - global CLIENT - if CLIENT.active_workspace is None: + if client.active_workspace is None: sublime.error_message( "You haven't joined any worksapce yet. \ use `Codemp: Join Workspace` or `Codemp: Join`" ) return - rt.dispatch(CLIENT.active_workspace.attach(buffer_id)) + tm.dispatch(client.active_workspace.attach(buffer_id)) def input_description(self): return "Join buffer in the active workspace" # This is awful, fix it def input(self, args): - global CLIENT - if CLIENT.active_workspace is None: + if client.active_workspace is None: sublime.error_message( "You haven't joined any worksapce yet. \ use `Codemp: Join Workspace` or `Codemp: Join`" @@ -228,7 +234,7 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand): return 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 RawBufferId() else: @@ -259,8 +265,7 @@ class ListBufferId(sublime_plugin.ListInputHandler): return "buffer_id" def list_items(self): - global CLIENT - return CLIENT.active_workspace.handle.filetree() + return client.active_workspace.handle.filetree() def next_input(self, args): if "buffer_id" not in args: @@ -313,7 +318,7 @@ class RawBufferId(sublime_plugin.TextInputHandler): ############################################################################# class CodempDisconnectCommand(sublime_plugin.WindowCommand): def run(self): - rt.sync(disconnect_client()) + tm.sync(disconnect_client()) # Proxy Commands ( NOT USED, left just in case we need it again. ) diff --git a/src/TaskManager.py b/src/TaskManager.py index 6cd47d3..a33ebd5 100644 --- a/src/TaskManager.py +++ b/src/TaskManager.py @@ -1,21 +1,29 @@ from typing import Optional import asyncio -import Codemp.ext.sublime_asyncio as rt +from ..ext import sublime_asyncio as rt class TaskManager: - def __init__(self, exit_handler): + def __init__(self): self.tasks = [] - self.exit_handler_id = rt.acquire(exit_handler) + self.runtime = rt + self.exit_handler_id = None + + def acquire(self, exit_handler): + if self.exit_handler_id is None: + # don't allow multiple exit handlers + self.exit_handler_id = self.runtime.acquire(exit_handler) + + return self.exit_handler_id def release(self, at_exit): - rt.release(at_exit=at_exit, exit_handler_id=self.exit_handler_id) + self.runtime.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 dispatch(self, coro, name=None): + self.runtime.dispatch(coro, self.store_named_lambda(name)) def sync(self, coro): - rt.sync(coro) + self.runtime.sync(coro) def remove_stopped(self): self.tasks = list(filter(lambda T: not T.cancelled(), self.tasks)) @@ -26,7 +34,7 @@ class TaskManager: self.tasks.append(task) self.remove_stopped() - def store_named_lambda(self, name): + def store_named_lambda(self, name=None): def _store(task): self.store(task, name) @@ -43,7 +51,7 @@ class TaskManager: def pop_task(self, name) -> Optional: idx = self.get_task_idx(name) if id is not None: - return self.task.pop(idx) + return self.tasks.pop(idx) return None async def _stop(self, task): @@ -56,8 +64,12 @@ class TaskManager: def stop(self, name): t = self.get_task(name) if t is not None: - rt.dispatch(self._stop(t)) + self.runtime.dispatch(self._stop(t)) def stop_all(self): for task in self.tasks: - rt.dispatch(self._stop(task)) + self.runtime.dispatch(self._stop(task)) + + +# singleton instance +tm = TaskManager() diff --git a/src/codemp_client.py b/src/client.py similarity index 56% rename from src/codemp_client.py rename to src/client.py index d7141a3..3763722 100644 --- a/src/codemp_client.py +++ b/src/client.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Optional, Callable - +from typing import Optional import sublime import asyncio @@ -8,11 +7,10 @@ import tempfile import os import shutil - -import Codemp.src.globals as g -from Codemp.src.wrappers import BufferController, Workspace, Client -from Codemp.src.utils import status_log, rowcol_to_region -from Codemp.src.TaskManager import TaskManager +from ..src import globals as g +from ..src.TaskManager import tm +from ..src.wrappers import BufferController, Workspace, Client +from ..src.utils import status_log, rowcol_to_region class CodempLogger: @@ -22,7 +20,7 @@ class CodempLogger: async def message(self): return await self.handle.message() - async def spawn_logger(self): + async def log(self): status_log("spinning up the logger...") try: while msg := await self.handle.message(): @@ -44,12 +42,11 @@ class VirtualBuffer: self, workspace: VirtualWorkspace, remote_id: str, - view: sublime.View, buffctl: BufferController, ): - self.view = view + self.view = sublime.active_window().new_file() self.codemp_id = remote_id - self.sublime_id = view.buffer_id() + self.sublime_id = self.view.buffer_id() self.workspace = workspace self.buffctl = buffctl @@ -61,6 +58,11 @@ class VirtualBuffer: self.view.retarget(self.tmpfile) self.view.set_scratch(True) + tm.dispatch( + self.apply_bufferchange_task(), + f"{g.BUFFCTL_TASK_PREFIX}-{self.codemp_id}", + ) + # mark the view as a codemp view s = self.view.settings() self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]") @@ -76,20 +78,74 @@ class VirtualBuffer: del s[g.CODEMP_REMOTE_ID] del s[g.CODEMP_WORKSPACE_ID] 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) + + tm.stop(f"{g.BUFFCTL_TASK_PREFIX}-{self.codemp_id}") status_log(f"cleaning up virtual buffer '{self.codemp_id}'") + async def apply_bufferchange_task(self): + status_log(f"spinning up '{self.codemp_id}' buffer worker...") + try: + while text_change := await self.buffctl.recv(): + if text_change.is_empty(): + status_log("change is empty. skipping.") + continue + # 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 self.view.id() == g.ACTIVE_CODEMP_VIEW: + self.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 + # a textcommand associated with a view. + self.view.run_command( + "codemp_replace_text", + { + "start": text_change.start_incl, + "end": text_change.end_excl, + "content": text_change.content, + "change_id": self.view.change_id(), + }, + ) + + except asyncio.CancelledError: + status_log(f"'{self.codemp_id}' buffer worker stopped...") + raise + except Exception as e: + status_log(f"buffer worker '{self.codemp_id}' crashed:\n{e}") + raise + + def send_buffer_change(self, changes): + # we do not do any index checking, and trust sublime with providing the correct + # sequential indexing, assuming the changes are applied in the order they are received. + for change in changes: + region = sublime.Region(change.a.pt, change.b.pt) + status_log( + "sending txt change: Reg({} {}) -> '{}'".format( + region.begin(), region.end(), change.str + ) + ) + self.buffctl.send(region.begin(), region.end(), change.str) + + def send_cursor(self, vws: VirtualWorkspace): + # TODO: only the last placed cursor/selection. + # status_log(f"sending cursor position in workspace: {vbuff.workspace.id}") + region = self.view.sel()[0] + start = self.view.rowcol(region.begin()) # only counts UTF8 chars + end = self.view.rowcol(region.end()) + + vws.curctl.send(self.codemp_id, start, end) + # 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): + def __init__(self, workspace_id: str, handle: Workspace): self.id = workspace_id self.sublime_window = sublime.active_window() - self.client = client self.handle = handle self.curctl = handle.cursor() + self.isactive = False # mapping remote ids -> local ids self.id_map: dict[str, str] = {} @@ -117,11 +173,9 @@ class VirtualWorkspace: s[g.CODEMP_WINDOW_TAG] = True s[g.CODEMP_WINDOW_WORKSPACES] = [self.id] - def add_buffer(self, remote_id: str, vbuff: VirtualBuffer): - self.id_map[remote_id] = vbuff.view.buffer_id() - self.active_buffers[vbuff.view.buffer_id()] = vbuff - def cleanup(self): + self.deactivate() + # 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.values(): @@ -132,7 +186,7 @@ class VirtualWorkspace: d = self.sublime_window.project_data() newf = list( filter( - lambda F: F["name"] != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}", + lambda f: f.get("name", "") != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}", d["folders"], ) ) @@ -145,11 +199,38 @@ class VirtualWorkspace: del s[g.CODEMP_WINDOW_TAG] del s[g.CODEMP_WINDOW_WORKSPACES] + def activate(self): + tm.dispatch( + self.move_cursor_task(), + f"{g.CURCTL_TASK_PREFIX}-{self.id}", + ) + self.isactive = True + + def deactivate(self): + if self.isactive: + tm.stop(f"{g.CURCTL_TASK_PREFIX}-{self.id}") + self.isactive = False + + def add_buffer(self, remote_id: str, vbuff: VirtualBuffer): + self.id_map[remote_id] = vbuff.view.buffer_id() + self.active_buffers[vbuff.view.buffer_id()] = vbuff + def get_by_local(self, local_id: str) -> Optional[VirtualBuffer]: return self.active_buffers.get(local_id) def get_by_remote(self, remote_id: str) -> Optional[VirtualBuffer]: - return self.active_buffers.get(self.id_map.get(remote_id)) + local_id = self.id_map.get(remote_id) + if local_id is None: + return + + vbuff = self.active_buffers.get(local_id) + if vbuff is None: + status_log( + "[WARN] a local-remote buffer id pair was found but not the matching virtual buffer." + ) + return + + return vbuff async def attach(self, id: str): if id is None: @@ -170,31 +251,80 @@ class VirtualWorkspace: status_log(f"error when attaching to buffer '{id}': {e}") return - view = self.sublime_window.new_file() - vbuff = VirtualBuffer(self, id, view, buff_ctl) + vbuff = VirtualBuffer(self, id, buff_ctl) self.add_buffer(id, vbuff) - self.client.spawn_buffer_manager(vbuff) - # TODO! if the view is already active calling focus_view() will not trigger the on_activate - self.sublime_window.focus_view(view) + self.sublime_window.focus_view(vbuff.view) + + async def move_cursor_task(self): + status_log(f"spinning up cursor worker for workspace '{self.id}'...") + try: + while cursor_event := await self.curctl.recv(): + vbuff = self.get_by_remote(cursor_event.buffer) + + if vbuff is None: + continue + + reg = rowcol_to_region(vbuff.view, cursor_event.start, cursor_event.end) + reg_flags = sublime.RegionFlags.DRAW_EMPTY # show cursors. + + user_hash = hash(cursor_event.user) + vbuff.view.add_regions( + f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}", + [reg], + flags=reg_flags, + scope=g.REGIONS_COLORS[user_hash % len(g.REGIONS_COLORS)], + annotations=[cursor_event.user], + annotation_color=g.PALETTE[user_hash % len(g.PALETTE)], + ) + + except asyncio.CancelledError: + status_log(f"cursor worker for '{self.id}' stopped...") + raise + except Exception as e: + status_log(f"cursor worker '{self.id}' crashed:\n{e}") + raise class VirtualClient: - def __init__(self, on_exit: Callable = None): + def __init__(self): self.handle: Client = Client() self.workspaces: dict[str, VirtualWorkspace] = {} - self.active_workspace: VirtualWorkspace = None - self.tm = TaskManager(on_exit) + self.active_workspace: Optional[VirtualWorkspace] = None def __getitem__(self, key: str): return self.workspaces.get(key) - def make_active(self, ws: VirtualWorkspace): + def get_workspace(self, view): + tag_id = view.settings().get(g.CODEMP_WORKSPACE_ID) + if tag_id is None: + return + + ws = self.workspaces.get(tag_id) + if ws is None: + status_log( + "[WARN] a tag on the view was found but not a matching workspace." + ) + return + + return ws + + def get_buffer(self, view): + ws = self.get_workspace(view) + return None if ws is None else ws.get_by_local(view.buffer_id()) + + def make_active(self, ws: VirtualWorkspace | None): + if self.active_workspace == ws: + return + if self.active_workspace is not None: - self.tm.stop(f"{g.CURCTL_TASK_PREFIX}-{self.active_workspace.id}") + self.active_workspace.deactivate() + + if ws is not None: + ws.activate() + self.active_workspace = ws - self.spawn_cursor_manager(ws) async def connect(self, server_host: str): status_log(f"Connecting to {server_host}") @@ -211,12 +341,14 @@ class VirtualClient: async def join_workspace( self, workspace_id: str, user="sublime2", password="***REMOVED***" - ) -> VirtualWorkspace: + ) -> Optional[VirtualWorkspace]: try: status_log(f"Logging into workspace: '{workspace_id}'") await self.handle.login(user, password, workspace_id) except Exception as e: - status_log(f"Failed to login to workspace '{workspace_id}'.\nerror: {e}", True) + status_log( + f"Failed to login to workspace '{workspace_id}'.\nerror: {e}", True + ) return try: @@ -226,105 +358,11 @@ class VirtualClient: status_log(f"Could not join workspace '{workspace_id}'.\nerror: {e}", True) return - vws = VirtualWorkspace(self, workspace_id, workspace_handle) - self.make_active(vws) + vws = VirtualWorkspace(workspace_id, workspace_handle) self.workspaces[workspace_id] = vws + self.make_active(vws) return vws - def spawn_cursor_manager(self, virtual_workspace: VirtualWorkspace): - async def move_cursor_task(vws): - status_log(f"spinning up cursor worker for workspace '{vws.id}'...") - try: - while cursor_event := await vws.curctl.recv(): - vbuff = vws.get_by_remote(cursor_event.buffer) - if vbuff is None: - continue - - reg = rowcol_to_region( - vbuff.view, cursor_event.start, cursor_event.end - ) - reg_flags = sublime.RegionFlags.DRAW_EMPTY # show cursors. - - user_hash = hash(cursor_event.user) - vbuff.view.add_regions( - f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}", - [reg], - flags=reg_flags, - scope=g.REGIONS_COLORS[user_hash % len(g.REGIONS_COLORS)], - annotations=[cursor_event.user], - annotation_color=g.PALETTE[user_hash % len(g.PALETTE)], - ) - - except asyncio.CancelledError: - status_log(f"cursor worker for '{vws.id}' stopped...") - raise - except Exception as e: - status_log(f"cursor worker '{vws.id}' crashed:\n{e}") - raise - - self.tm.dispatch( - move_cursor_task(virtual_workspace), - f"{g.CURCTL_TASK_PREFIX}-{virtual_workspace.id}", - ) - - def send_cursor(self, vbuff: VirtualBuffer): - # TODO: only the last placed cursor/selection. - # status_log(f"sending cursor position in workspace: {vbuff.workspace.id}") - region = vbuff.view.sel()[0] - start = vbuff.view.rowcol(region.begin()) # only counts UTF8 chars - end = vbuff.view.rowcol(region.end()) - - vbuff.workspace.curctl.send(vbuff.codemp_id, start, end) - - def spawn_buffer_manager(self, vbuff: VirtualBuffer): - 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(): - if text_change.is_empty(): - status_log("change is empty. skipping.") - continue - # 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 vb.view.id() == g.ACTIVE_CODEMP_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 - # a textcommand associated with a view. - vb.view.run_command( - "codemp_replace_text", - { - "start": text_change.start_incl, - "end": text_change.end_excl, - "content": text_change.content, - "change_id": vb.view.change_id(), - }, - ) - - except asyncio.CancelledError: - status_log(f"'{vb.codemp_id}' buffer worker stopped...") - raise - except Exception as e: - status_log(f"buffer worker '{vb.codemp_id}' crashed:\n{e}") - raise - - self.tm.dispatch( - apply_buffer_change_task(vbuff), - f"{g.BUFFCTL_TASK_PREFIX}-{vbuff.codemp_id}", - ) - - def send_buffer_change(self, changes, vbuff: VirtualBuffer): - # we do not do any index checking, and trust sublime with providing the correct - # sequential indexing, assuming the changes are applied in the order they are received. - for change in changes: - region = sublime.Region(change.a.pt, change.b.pt) - status_log( - "sending txt change: Reg({} {}) -> '{}'".format( - region.begin(), region.end(), change.str - ) - ) - vbuff.buffctl.send(region.begin(), region.end(), change.str) +client = VirtualClient() diff --git a/src/globals.py b/src/globals.py index 55fd01d..a0e0de5 100644 --- a/src/globals.py +++ b/src/globals.py @@ -12,7 +12,9 @@ WORKSPACE_FOLDER_PREFIX = "CODEMP::" SUBLIME_REGIONS_PREFIX = "codemp-cursors" SUBLIME_STATUS_ID = "z_codemp_buffer" CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event" + ACTIVE_CODEMP_VIEW = None +ACTIVE_CODEMP_WINDOW = None PALETTE = [ "var(--redish)", diff --git a/src/utils.py b/src/utils.py index f26a449..d7fc52a 100644 --- a/src/utils.py +++ b/src/utils.py @@ -16,10 +16,15 @@ def rowcol_to_region(view, start, end): def safe_listener_detach(txt_listener: sublime_plugin.TextChangeListener): - if txt_listener.is_attached(): + if txt_listener is not None and txt_listener.is_attached(): txt_listener.detach() +def safe_listener_attach(txt_listener: sublime_plugin.TextChangeListener, buffer): + if txt_listener is not None and not txt_listener.is_attached(): + txt_listener.attach(buffer) + + def get_contents(view): r = sublime.Region(0, view.size()) return view.substr(r)