from __future__ import annotations from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from .workspace import WorkspaceManager import codemp import sublime import os import logging import threading from codemp import TextChange from .. import globals as g from ..utils import populate_view from ..utils import get_contents from ..utils import safe_listener_attach from ..utils import safe_listener_detach from ..utils import bidict logger = logging.getLogger(__name__) def bind_callback(v: sublime.View): # we need this lock to prevent multiple instance of try_recv() to spin up # which would cause out of order insertion of changes. multi_tryrecv_lock = threading.Lock() def _callback(bufctl: codemp.BufferController): def _(): try: # change_id = v.change_id() change_id = None while buffup := bufctl.try_recv().wait(): logger.debug("received remote buffer change!") if buffup is None: break if buffup.change.is_empty(): logger.debug("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 v == sublime.active_window().active_view(): v.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. change = buffup.change v.run_command( "codemp_replace_text", { "start": change.start_idx, "end": change.end_idx, "content": change.content, "change_id": change_id, }, # pyright: ignore ) bufctl.ack(buffup.version) except Exception as e: raise e finally: logger.debug("releasing lock") multi_tryrecv_lock.release() if multi_tryrecv_lock.acquire(blocking=False): logger.debug("acquiring lock") sublime.set_timeout(_) return _callback class BufferManager(): def __init__(self, handle: codemp.BufferController, v: sublime.View, filename: str): self.handle: codemp.BufferController = handle self.view: sublime.View = v self.id = self.handle.path() self.filename = filename self.handle.callback(bind_callback(self.view)) def __del__(self): logger.debug(f"dropping buffer {self.id}") self.handle.clear_callback() def __hash__(self): return hash(self.id) def send_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) # logger.debug( # "sending txt change: Reg({} {}) -> '{}'".format( # region.begin(), region.end(), change.str # ) # ) # we must block and wait the send request to make sure the change went through ok self.handle.send(TextChange(start=region.begin(), end=region.end(), content=change.str)) def sync(self, text_listener): promise = self.handle.content() def _(): content = promise.wait() current_contents = get_contents(self.view) if content == current_contents: return safe_listener_detach(text_listener) populate_view(self.view, content) safe_listener_attach(text_listener, self.view.buffer()) sublime.status_message("Syncd contents.") sublime.set_timeout_async(_) class BufferRegistry(): def __init__(self): self._buffers: bidict[BufferManager, WorkspaceManager] = bidict() def lookup(self, ws: Optional[WorkspaceManager] = None) -> list[BufferManager]: if not ws: return list(self._buffers.keys()) bfs = self._buffers.inverse.get(ws) return bfs if bfs else [] def lookupParent(self, bf: BufferManager | str) -> WorkspaceManager: if isinstance(bf, str): bf = self.lookupId(bf) return self._buffers[bf] def lookupId(self, bid: str) -> BufferManager: bfm = next((bf for bf in self._buffers if bf.id == bid), None) if not bfm: raise KeyError return bfm def add(self, bhandle: codemp.BufferController, wsm: WorkspaceManager): bid = bhandle.path() # tmpfile = os.path.join(wsm.rootdir, bid) # open(tmpfile, "a").close() win = sublime.active_window() view = win.open_file(bid) view.set_scratch(True) # view.retarget(tmpfile) view.settings().set(g.CODEMP_VIEW_TAG, True) view.settings().set(g.CODEMP_BUFFER_ID, bid) view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]") tmpfile = "DISABLE" bfm = BufferManager(bhandle, view, tmpfile) self._buffers[bfm] = wsm return bfm def remove(self, bf: BufferManager | str): if isinstance(bf, str): bf = self.lookupId(bf) del self._buffers[bf] bf.view.close() buffers = BufferRegistry()