diff --git a/main.py b/main.py index 86ad0a9..be9fb52 100644 --- a/main.py +++ b/main.py @@ -94,9 +94,11 @@ class CodempBrowseServerCommand(sublime_plugin.WindowCommand): class CodempReplaceTextCommand(sublime_plugin.TextCommand): - def run(self, edit, start, end, content, change_id): + def run(self, edit, start, end, content, change_id = None): # we modify the region to account for any change that happened in the mean time - region = self.view.transform_region_from(sublime.Region(start, end), change_id) + region = sublime.Region(start, end) + if change_id: + region = self.view.transform_region_from(sublime.Region(start, end), change_id) self.view.replace(edit, region, content) @@ -155,7 +157,7 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): return vws.send_cursor(vbuff.id, start, end) - logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}") + # logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}") def on_activated(self): logger.debug(f"'{self.view}' view activated!") diff --git a/plugin/commands/workspace.py b/plugin/commands/workspace.py index ab6a59f..1e45924 100644 --- a/plugin/commands/workspace.py +++ b/plugin/commands/workspace.py @@ -7,7 +7,7 @@ from ..core.workspace import workspaces from ..core.buffers import buffers from ..text_listener import TEXT_LISTENER -from ..utils import safe_listener_attach, safe_listener_detach +from ..utils import safe_listener_attach, safe_listener_detach, populate_view from ..input_handlers import SimpleListInput, SimpleTextInput logger = logging.getLogger(__name__) @@ -67,11 +67,11 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand): # now we can defer the attaching process logger.debug(f"attempting to attach to {buffer_id}...") - promise = vws.handle.attach_buffer(buffer_id) + ctl_promise = vws.handle.attach_buffer(buffer_id) def _(): try: - buff_ctl = promise.wait() + buff_ctl = ctl_promise.wait() logger.debug("attach successfull!") except Exception as e: logging.error(f"error when attaching to buffer '{id}':\n\n {e}") @@ -79,8 +79,11 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand): return safe_listener_detach(TEXT_LISTENER) + content_promise = buff_ctl.content() vbuff = buffers.add(buff_ctl, vws) + content = content_promise.wait() + populate_view(vbuff.view, content) if self.window.active_view() == vbuff.view: # if view is already active focusing it won't trigger `on_activate`. safe_listener_attach(TEXT_LISTENER, vbuff.view.buffer()) @@ -115,8 +118,8 @@ class CodempLeaveBufferCommand(sublime_plugin.WindowCommand): buffers.lookupId(buffer_id) vws = workspaces.lookupId(workspace_id) except KeyError: - sublime.error_message(f"You are not attached to the buffer '{id}'") - logging.warning(f"You are not attached to the buffer '{id}'") + sublime.error_message(f"You are not attached to the buffer '{buffer_id}'") + logging.warning(f"You are not attached to the buffer '{buffer_id}'") return if not vws.handle.get_buffer(buffer_id): diff --git a/plugin/core/buffers.py b/plugin/core/buffers.py index 4995293..697bc8e 100644 --- a/plugin/core/buffers.py +++ b/plugin/core/buffers.py @@ -7,10 +7,12 @@ if TYPE_CHECKING: 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 @@ -18,44 +20,54 @@ 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 _(): - change_id = v.change_id() - while buffup := bufctl.try_recv().wait(): - logger.debug("received remote buffer change!") - if buffup is None: - break + 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 + 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 + # 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. - - # 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. - try: change = buffup.change v.run_command( "codemp_replace_text", { - "start": change.start, - "end": change.end, + "start": change.start_idx, + "end": change.end_idx, "content": change.content, "change_id": change_id, }, # pyright: ignore ) - except Exception as e: - raise e - bufctl.ack(buffup.version) - sublime.set_timeout(_) + 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(): @@ -78,11 +90,11 @@ class BufferManager(): # 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 - ) - ) + # 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)) @@ -91,9 +103,14 @@ class BufferManager(): 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(): @@ -120,8 +137,7 @@ class BufferRegistry(): bid = bhandle.path() # tmpfile = os.path.join(wsm.rootdir, bid) # open(tmpfile, "a").close() - content = bhandle.content() - + win = sublime.active_window() view = win.open_file(bid) view.set_scratch(True) @@ -129,7 +145,6 @@ class BufferRegistry(): view.settings().set(g.CODEMP_VIEW_TAG, True) view.settings().set(g.CODEMP_BUFFER_ID, bid) view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]") - populate_view(view, content.wait()) tmpfile = "DISABLE" bfm = BufferManager(bhandle, view, tmpfile) diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index cc213b6..5370b84 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -51,7 +51,7 @@ def cursor_callback(ctl: codemp.CursorController): def _(): while event := ctl.try_recv().wait(): if event is None: break - + try: bfm = buffers.lookupId(event.sel.buffer) except KeyError: continue diff --git a/plugin/text_listener.py b/plugin/text_listener.py index bd57685..9443ebd 100644 --- a/plugin/text_listener.py +++ b/plugin/text_listener.py @@ -1,3 +1,4 @@ +import sublime import sublime_plugin import logging diff --git a/plugin/utils.py b/plugin/utils.py index 16e9a72..4f8ae27 100644 --- a/plugin/utils.py +++ b/plugin/utils.py @@ -88,7 +88,7 @@ def populate_view(view, content): "start": 0, "end": view.size(), "content": content, - "change_id": view.change_id(), + "change_id": None, }, ) @@ -105,7 +105,7 @@ def draw_cursor_region(view, start, end, user): reg_flags = sublime.RegionFlags.DRAW_EMPTY user_hash = hash(user) - + view.add_regions( f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}", [reg],