wip: continuing refactor, added buffers

Former-commit-id: 96208df478a2b9534668331d6a25118778822f9b
This commit is contained in:
cschen 2024-09-18 16:57:26 +02:00
parent 539a4e4e5e
commit c203524ced
6 changed files with 117 additions and 201 deletions

View file

@ -1,5 +1,3 @@
# pyright: ignore[reportIncompatibleMethodOverride]
import sublime import sublime
import sublime_plugin import sublime_plugin
import logging import logging
@ -8,18 +6,14 @@ import random
from ...lib import codemp from ...lib import codemp
from ..core.session import session from ..core.session import session
from ..core.registry import workspaces from ..core.registry import workspaces
from ..core.registry import buffers
from input_handlers import SimpleTextInput from input_handlers import SimpleTextInput
from input_handlers import SimpleListInput from input_handlers import SimpleListInput
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Client Commands
#############################################################################
# Connect Command
class CodempConnectCommand(sublime_plugin.WindowCommand): class CodempConnectCommand(sublime_plugin.WindowCommand):
def is_enabled(self) -> bool:
return True
def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride] def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride]
def _(): def _():
@ -65,10 +59,14 @@ class CodempDisconnectCommand(sublime_plugin.WindowCommand):
return session.is_active() return session.is_active()
def run(self): def run(self):
for ws in workspaces.lookup(): cli = session.client
ws.uninstall() assert cli is not None
session.disconnect() for ws in workspaces.lookup():
if cli.leave_workspace(ws.id):
workspaces.remove(ws)
session.drop_client()
# Join Workspace Command # Join Workspace Command

View file

@ -1,17 +1,23 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .workspace import WorkspaceManager
from ...lib import codemp
import sublime import sublime
import os import os
import logging import logging
from . import globals as g from .. import globals as g
from .utils import populate_view, safe_listener_attach, safe_listener_detach from ..utils import populate_view
import codemp from ..utils import safe_listener_attach
from ..utils import safe_listener_detach
from ..utils import bidict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def make_bufferchange_cb(buff: VirtualBuffer): def bind_buffer_manager(buff: BufferManager):
def __callback(bufctl: codemp.BufferController): def text_callback(bufctl: codemp.BufferController):
def _(): def _():
change_id = buff.view.change_id() change_id = buff.view.change_id()
while change := bufctl.try_recv().wait(): while change := bufctl.try_recv().wait():
@ -41,70 +47,25 @@ def make_bufferchange_cb(buff: VirtualBuffer):
"change_id": change_id, "change_id": change_id,
}, # pyright: ignore }, # pyright: ignore
) )
sublime.set_timeout(_) sublime.set_timeout(_)
return __callback return text_callback
class BufferManager():
class VirtualBuffer: def __init__(self, handle: codemp.BufferController, v: sublime.View, filename: str):
def __init__( self.handle: codemp.BufferController = handle
self, self.view: sublime.View = v
buffctl: codemp.BufferController, self.id = self.handle.path()
view: sublime.View, self.filename = filename
rootdir: str,
):
self.buffctl = buffctl
self.view = view
self.id = self.buffctl.path()
self.tmpfile = os.path.join(rootdir, self.id)
open(self.tmpfile, "a").close()
self.view.set_scratch(True)
self.view.set_name(self.id)
self.view.retarget(self.tmpfile)
self.view.settings().set(g.CODEMP_BUFFER_TAG, True)
self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
logger.info(f"registering a callback for buffer: {self.id}")
self.buffctl.callback(make_bufferchange_cb(self))
self.isactive = True
def __del__(self): def __del__(self):
logger.debug("__del__ buffer called.") logger.debug(f"dropping buffer {self.id}")
self.handle.clear_callback()
self.handle.stop()
def __hash__(self) -> int: def __hash__(self):
return hash(self.id) return hash(self.id)
def uninstall(self): def send_change(self, changes):
logger.info(f"clearing a callback for buffer: {self.id}")
self.buffctl.clear_callback()
self.buffctl.stop()
self.isactive = False
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)
def sync(self, text_listener):
promise = self.buffctl.content()
def _():
content = promise.wait()
safe_listener_detach(text_listener)
populate_view(self.view, content)
safe_listener_attach(text_listener, self.view.buffer())
sublime.set_timeout_async(_)
def send_buffer_change(self, changes):
# we do not do any index checking, and trust sublime with providing the correct # 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. # sequential indexing, assuming the changes are applied in the order they are received.
for change in changes: for change in changes:
@ -114,6 +75,58 @@ class VirtualBuffer:
region.begin(), region.end(), change.str region.begin(), region.end(), change.str
) )
) )
# we must block and wait the send request to make sure the change went through ok # 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() self.handle.send(region.begin(), region.end(), change.str).wait()
def sync(self, text_listener):
promise = self.handle.content()
def _():
content = promise.wait()
safe_listener_detach(text_listener)
populate_view(self.view, content)
safe_listener_attach(text_listener, self.view.buffer())
sublime.set_timeout_async(_)
class BufferRegistry():
def __init__(self):
self._buffers: bidict[BufferManager, WorkspaceManager] = bidict()
def lookup(self, ws: WorkspaceManager | None = None) -> list[BufferManager]:
if not ws:
return list(self._buffers.keys())
bf = self._buffers.inverse.get(ws)
return bf if bf else []
def lookupId(self, bid: str) -> BufferManager | None:
return next((bf for bf in self._buffers if bf.id == bid), None)
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]")
bfm = BufferManager(bhandle, view, tmpfile)
def remove(self, bf: BufferManager | str | None):
if isinstance(bf, str):
bf = self.lookupId(bf)
if not bf: return
del self._buffers[bf]
bf.view.close()

View file

@ -2,4 +2,4 @@ from .workspace import WorkspaceRegistry
from .buffers import BufferRegistry from .buffers import BufferRegistry
workspaces = WorkspaceRegistry() workspaces = WorkspaceRegistry()
buffers = BufferRegistry() buffers = BufferRegistry()

View file

@ -9,10 +9,11 @@ class SessionManager():
self._driver = None self._driver = None
self._client = None self._client = None
def is_init(self):
return self._running and self._driver is not None
def is_active(self): def is_active(self):
return self._driver is not None \ return self.is_init() and self._client is not None
and self._running \
and self._client is not None
@property @property
def client(self): def client(self):
@ -37,23 +38,20 @@ class SessionManager():
if not self._driver: if not self._driver:
return return
self.disconnect() self.drop_client()
self._driver.stop() self._driver.stop()
self._running = False self._running = False
self._driver = None self._driver = None
def connect(self, config: codemp.Config) -> codemp.Client: def connect(self, config: codemp.Config) -> codemp.Client:
if not self._running: if not self.is_init():
self.get_or_init() self.get_or_init()
self._client = codemp.connect(config).wait() self._client = codemp.connect(config).wait()
logger.debug(f"Connected to '{config.host}' as user {self._client.user_name} (id: {self._client.user_id})") logger.debug(f"Connected to '{config.host}' as user {self._client.user_name} (id: {self._client.user_id})")
return self._client return self._client
def disconnect(self): def drop_client(self):
if not self._client:
return
self._client = None self._client = None

View file

@ -11,118 +11,16 @@ import tempfile
import logging import logging
from .. import globals as g from .. import globals as g
from .buffers import VirtualBuffer
from ..utils import draw_cursor_region from ..utils import draw_cursor_region
from ..utils import bidict from ..utils import bidict
from ..core.registry import buffers from ..core.registry import buffers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# def make_cursor_callback(workspace: VirtualWorkspace):
# def _callback(ctl: codemp.CursorController):
# def _():
# while event := ctl.try_recv().wait():
# logger.debug("received remote cursor movement!")
# if event is None:
# break
# vbuff = workspace.buff_by_id(event.buffer)
# if vbuff is None:
# logger.warning(
# f"{workspace.id} received a cursor event for a buffer that wasn't saved internally."
# )
# continue
# draw_cursor_region(vbuff.view, event.start, event.end, event.user)
# sublime.set_timeout_async(_)
# return _callback
# # A virtual workspace is a bridge class that aims to translate
# # events that happen to the codemp workspaces into sublime actions
# class VirtualWorkspace:
# def __init__(self, handle: codemp.Workspace, window: sublime.Window):
# self.handle: codemp.Workspace = handle
# self.window: sublime.Window = window
# self.curctl: codemp.CursorController = self.handle.cursor()
# self.id: str = self.handle.id()
# self.handle.fetch_buffers()
# self.handle.fetch_users()
# self._id2buff: dict[str, VirtualBuffer] = {}
# tmpdir = tempfile.mkdtemp(prefix="codemp_")
# self.rootdir = tmpdir
# proj: dict = self.window.project_data() # pyright: ignore
# if proj is None:
# proj = {"folders": []} # pyright: ignore, Value can be None
# proj["folders"].append(
# {"name": f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}", "path": self.rootdir}
# )
# self.window.set_project_data(proj)
# self.curctl.callback(make_cursor_callback(self))
# self.isactive = True
# def __del__(self):
# logger.debug("workspace destroyed!")
# def __hash__(self) -> int:
# # so we can use these as dict keys!
# return hash(self.id)
# def uninstall(self):
# self.curctl.clear_callback()
# self.isactive = False
# self.curctl.stop()
# for vbuff in self._id2buff.values():
# vbuff.uninstall()
# if not self.handle.detach(vbuff.id):
# logger.warning(
# f"could not detach from '{vbuff.id}' for workspace '{self.id}'."
# )
# self._id2buff.clear()
# proj: dict = self.window.project_data() # type:ignore
# if proj is None:
# raise
# clean_proj_folders = list(
# filter(
# lambda f: f.get("name", "") != f"{g.WORKSPACE_FOLDER_PREFIX}{self.id}",
# proj["folders"],
# )
# )
# proj["folders"] = clean_proj_folders
# self.window.set_project_data(proj)
# logger.info(f"cleaning up virtual workspace '{self.id}'")
# shutil.rmtree(self.rootdir, ignore_errors=True)
# def install_buffer(
# self, buff: codemp.BufferController, listener: CodempClientTextChangeListener
# ) -> VirtualBuffer:
# logger.debug(f"installing buffer {buff.path()}")
# view = self.window.new_file()
# vbuff = VirtualBuffer(buff, view, self.rootdir)
# self._id2buff[vbuff.id] = vbuff
# vbuff.sync(listener)
# return vbuff
def add_project_folder(w: sublime.Window, folder: str, name: str = ""): def add_project_folder(w: sublime.Window, folder: str, name: str = ""):
proj: dict = w.project_data() # pyright: ignore proj: dict = w.project_data() # pyright: ignore
if proj is None: if proj is None:
proj = {"folders": []} # pyright: ignore, Value can be None proj = {"folders": []} # pyright: ignore, `Value` can be None
if name == "": if name == "":
entry = {"path": folder} entry = {"path": folder}
@ -147,35 +45,46 @@ def remove_project_folder(w: sublime.Window, filterstr: str):
proj["folders"] = clean_proj_folders proj["folders"] = clean_proj_folders
w.set_project_data(proj) w.set_project_data(proj)
def cursor_callback(ctl: codemp.CursorController):
def _():
while event := ctl.try_recv().wait():
if event is None: break
bfm = buffers.lookupId(event.buffer)
if not bfm: continue
draw_cursor_region(bfm.view, event.start, event.end, event.user)
sublime.set_timeout_async(_)
class WorkspaceManager(): class WorkspaceManager():
def __init__(self, handle: codemp.Workspace, window: sublime.Window, rootdir: str) -> None: def __init__(self, handle: codemp.Workspace, window: sublime.Window, rootdir: str) -> None:
self.handle: codemp.Workspace = handle self.handle: codemp.Workspace = handle
self.window: sublime.Window = window self.window: sublime.Window = window
self.curctl: codemp.CursorController = self.handle.cursor() self.curctl: codemp.CursorController = self.handle.cursor()
self.rootdir: str = rootdir self.rootdir: str = rootdir
self.id: str = self.handle.id()
self.id = self.handle.id()
def __del__(self): def __del__(self):
logger.debug(f"dropping workspace {self.id}")
self.curctl.clear_callback() self.curctl.clear_callback()
self.curctl.stop() self.curctl.stop()
# TODO: STUFF WITH THE BUFFERS IN THE REGISTRY
for buff in self.handle.buffer_list(): for buff in self.handle.buffer_list():
if not self.handle.detach(buff): if not self.handle.detach(buff):
logger.warning( logger.warning(
f"could not detach from '{buff}' for workspace '{self.id}'." f"could not detach from '{buff}' for workspace '{self.id}'."
) )
bfm = buffers.lookupId(buff)
if bfm:
buffers.remove(bfm)
def send_cursor(self, id: str, start: Tuple[int, int], end: Tuple[int, int]): def send_cursor(self, id: str, start: Tuple[int, int], end: Tuple[int, int]):
# we can safely ignore the promise, we don't really care if everything # we can safely ignore the promise, we don't really care if everything
# is ok for now with the cursor. # is ok for now with the cursor.
self.curctl.send(id, start, end) self.curctl.send(id, start, end)
class WorkspaceRegistry(): class WorkspaceRegistry():
def __init__(self) -> None: def __init__(self) -> None:
self._workspaces: bidict[WorkspaceManager, sublime.Window] = bidict() self._workspaces: bidict[WorkspaceManager, sublime.Window] = bidict()
@ -189,7 +98,7 @@ class WorkspaceRegistry():
def lookupId(self, wid: str) -> WorkspaceManager | None: def lookupId(self, wid: str) -> WorkspaceManager | None:
return next((ws for ws in self._workspaces if ws.id == wid), None) return next((ws for ws in self._workspaces if ws.id == wid), None)
def add(self, wshandle: codemp.Workspace): def add(self, wshandle: codemp.Workspace) -> WorkspaceManager:
win = sublime.active_window() win = sublime.active_window()
tmpdir = tempfile.mkdtemp(prefix="codemp_") tmpdir = tempfile.mkdtemp(prefix="codemp_")
@ -198,13 +107,13 @@ class WorkspaceRegistry():
wm = WorkspaceManager(wshandle, win, tmpdir) wm = WorkspaceManager(wshandle, win, tmpdir)
self._workspaces[wm] = win self._workspaces[wm] = win
return wm
def remove(self, ws: WorkspaceManager | str | None): def remove(self, ws: WorkspaceManager | str | None):
if isinstance(ws, str): if isinstance(ws, str):
ws = self.lookupId(ws) ws = self.lookupId(ws)
if not ws: if not ws: return
return
remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}") remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}")
shutil.rmtree(ws.rootdir, ignore_errors=True) shutil.rmtree(ws.rootdir, ignore_errors=True)

View file

@ -1,16 +1,14 @@
BUFFCTL_TASK_PREFIX = "buffer-ctl" BUFFCTL_TASK_PREFIX = "buffer-ctl"
CURCTL_TASK_PREFIX = "cursor-ctl" CURCTL_TASK_PREFIX = "cursor-ctl"
CODEMP_BUFFER_TAG = "codemp-buffer" CODEMP_VIEW_TAG = "codemp-buffer"
CODEMP_REMOTE_ID = "codemp-buffer-id" CODEMP_BUFFER_ID = "codemp-buffer-id"
CODEMP_WORKSPACE_ID = "codemp-workspace-id" CODEMP_WORKSPACE_ID = "codemp-workspace-id"
CODEMP_WINDOW_TAG = "codemp-window"
CODEMP_WINDOW_WORKSPACES = "codemp-workspaces"
WORKSPACE_FOLDER_PREFIX = "CODEMP::" WORKSPACE_FOLDER_PREFIX = "CODEMP::"
SUBLIME_REGIONS_PREFIX = "codemp-cursors" SUBLIME_REGIONS_PREFIX = "codemp-cursors"
SUBLIME_STATUS_ID = "z_codemp_buffer" SUBLIME_STATUS_ID = "z_codemp_buffer"
CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event" CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event"
ACTIVE_CODEMP_VIEW = None ACTIVE_CODEMP_VIEW = None