mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2025-03-29 21:01:34 +01:00
Also improves the logic in the qpbrowser for the creation and deletion of buffers. and simplify the logic in the internal commands.
182 lines
6.2 KiB
Python
182 lines
6.2 KiB
Python
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 _innercb():
|
|
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(_innercb)
|
|
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.view.close()
|
|
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 _():
|
|
current_contents = get_contents(self.view)
|
|
content = promise.wait()
|
|
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 __contains__(self, item: str):
|
|
try: self.lookupId(item)
|
|
except KeyError: return False
|
|
return True
|
|
|
|
def hasactive(self):
|
|
return len(self._buffers.keys()) > 0
|
|
|
|
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 register(self, bhandle: codemp.BufferController, wsm: WorkspaceManager):
|
|
bid = bhandle.path()
|
|
|
|
win = sublime.active_window()
|
|
newfileflags = sublime.NewFileFlags.TRANSIENT \
|
|
| sublime.NewFileFlags.ADD_TO_SELECTION \
|
|
| sublime.NewFileFlags.FORCE_CLONE
|
|
view = win.new_file(newfileflags)
|
|
|
|
|
|
view.set_scratch(True)
|
|
view.set_name(os.path.basename(bid))
|
|
syntax = sublime.find_syntax_for_file(bid)
|
|
if syntax:
|
|
view.assign_syntax(syntax)
|
|
|
|
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]
|
|
|
|
|
|
buffers = BufferRegistry()
|
|
|
|
|
|
|
|
|