2024-08-29 08:17:52 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-08-09 15:54:12 +02:00
|
|
|
import sublime
|
|
|
|
import os
|
2024-08-09 19:20:58 +02:00
|
|
|
import logging
|
2024-08-09 15:54:12 +02:00
|
|
|
|
2024-08-23 20:59:06 +02:00
|
|
|
import codemp
|
2024-08-09 15:54:12 +02:00
|
|
|
from Codemp.src import globals as g
|
2024-09-02 11:38:11 +02:00
|
|
|
from Codemp.src.utils import populate_view, safe_listener_attach, safe_listener_detach
|
2024-08-09 19:20:58 +02:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2024-08-09 15:54:12 +02:00
|
|
|
|
|
|
|
|
2024-08-29 08:17:52 +02:00
|
|
|
def make_bufferchange_cb(buff: VirtualBuffer):
|
|
|
|
def __callback(bufctl: codemp.BufferController):
|
|
|
|
def get_change_and_apply():
|
|
|
|
change_id = buff.view.change_id()
|
|
|
|
while change := bufctl.try_recv().wait():
|
|
|
|
logger.debug("received remote buffer change!")
|
|
|
|
if change is None:
|
|
|
|
break
|
|
|
|
|
|
|
|
if 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 buff.view.id() == g.ACTIVE_CODEMP_VIEW:
|
|
|
|
buff.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.
|
|
|
|
buff.view.run_command(
|
|
|
|
"codemp_replace_text",
|
|
|
|
{
|
|
|
|
"start": change.start,
|
|
|
|
"end": change.end,
|
|
|
|
"content": change.content,
|
|
|
|
"change_id": change_id,
|
|
|
|
}, # pyright: ignore
|
|
|
|
)
|
|
|
|
|
|
|
|
sublime.set_timeout(get_change_and_apply)
|
|
|
|
|
|
|
|
return __callback
|
|
|
|
|
|
|
|
|
2024-08-09 15:54:12 +02:00
|
|
|
# This class is used as an abstraction between the local buffers (sublime side) and the
|
|
|
|
# remote buffers (codemp side), to handle the syncronicity.
|
|
|
|
# This class is mainly manipulated by a VirtualWorkspace, that manages its buffers
|
|
|
|
# using this abstract class
|
|
|
|
class VirtualBuffer:
|
|
|
|
def __init__(
|
|
|
|
self,
|
2024-08-23 20:59:06 +02:00
|
|
|
buffctl: codemp.BufferController,
|
2024-08-31 15:24:22 +02:00
|
|
|
view: sublime.View,
|
|
|
|
rootdir: str,
|
2024-08-09 15:54:12 +02:00
|
|
|
):
|
|
|
|
self.buffctl = buffctl
|
2024-08-23 20:59:06 +02:00
|
|
|
self.view = view
|
|
|
|
self.id = self.buffctl.name()
|
2024-08-09 15:54:12 +02:00
|
|
|
|
2024-08-23 20:59:06 +02:00
|
|
|
self.tmpfile = os.path.join(rootdir, self.id)
|
2024-08-09 15:54:12 +02:00
|
|
|
open(self.tmpfile, "a").close()
|
|
|
|
|
2024-09-02 11:38:11 +02:00
|
|
|
self.view.set_scratch(True)
|
2024-08-23 20:59:06 +02:00
|
|
|
self.view.set_name(self.id)
|
|
|
|
self.view.retarget(self.tmpfile)
|
2024-08-09 15:54:12 +02:00
|
|
|
|
|
|
|
s = self.view.settings()
|
|
|
|
self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
|
|
|
|
s[g.CODEMP_BUFFER_TAG] = True
|
|
|
|
|
2024-08-23 20:59:06 +02:00
|
|
|
logger.info(f"registering a callback for buffer: {self.id}")
|
2024-08-29 08:17:52 +02:00
|
|
|
self.buffctl.callback(make_bufferchange_cb(self))
|
2024-08-23 20:59:06 +02:00
|
|
|
self.isactive = True
|
|
|
|
|
2024-08-31 15:24:22 +02:00
|
|
|
def __del__(self):
|
2024-09-02 11:38:11 +02:00
|
|
|
logger.debug("__del__ buffer called.")
|
|
|
|
|
|
|
|
def __hash__(self) -> int:
|
|
|
|
return hash(self.id)
|
|
|
|
|
|
|
|
def uninstall(self):
|
2024-08-23 20:59:06 +02:00
|
|
|
logger.info(f"clearing a callback for buffer: {self.id}")
|
|
|
|
self.buffctl.clear_callback()
|
2024-08-31 15:24:22 +02:00
|
|
|
self.buffctl.stop()
|
2024-08-23 20:59:06 +02:00
|
|
|
self.isactive = False
|
|
|
|
|
2024-08-31 15:24:22 +02:00
|
|
|
os.remove(self.tmpfile)
|
|
|
|
|
|
|
|
def onclose(did_close):
|
|
|
|
if did_close:
|
|
|
|
logger.info(f"'{self.id}' closed successfully")
|
|
|
|
else:
|
|
|
|
logger.info(f"failed to close the view for '{self.id}'")
|
|
|
|
|
|
|
|
self.view.close(onclose)
|
|
|
|
|
2024-09-02 11:38:11 +02:00
|
|
|
def sync(self, text_listener):
|
2024-08-31 15:24:22 +02:00
|
|
|
promise = self.buffctl.content()
|
|
|
|
|
|
|
|
def defer_sync(promise):
|
|
|
|
content = promise.wait()
|
2024-09-02 11:38:11 +02:00
|
|
|
safe_listener_detach(text_listener)
|
2024-08-31 15:24:22 +02:00
|
|
|
populate_view(self.view, content)
|
2024-09-02 11:38:11 +02:00
|
|
|
safe_listener_attach(text_listener, self.view.buffer())
|
2024-08-31 15:24:22 +02:00
|
|
|
|
|
|
|
sublime.set_timeout_async(lambda: defer_sync(promise))
|
|
|
|
|
2024-08-23 20:59:06 +02:00
|
|
|
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)
|
|
|
|
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.buffctl.send(region.begin(), region.end(), change.str).wait()
|