codemp-sublime/plugin/core/buffers.py
cschen 0a25a5f7c1 Make create buffer and delete buffer, be internal commands. Dealing with the input handlers sheningans is too much.
Also improves the logic in the qpbrowser for the creation and deletion of buffers.
and simplify the logic in the internal commands.
2025-02-18 20:25:26 +01:00

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()