diff --git a/plugin/commands/client.py b/plugin/commands/client.py index 16db9dd..ca63cda 100644 --- a/plugin/commands/client.py +++ b/plugin/commands/client.py @@ -1,5 +1,3 @@ -# pyright: ignore[reportIncompatibleMethodOverride] - import sublime import sublime_plugin import logging @@ -8,18 +6,14 @@ import random from ...lib import codemp from ..core.session import session from ..core.registry import workspaces +from ..core.registry import buffers from input_handlers import SimpleTextInput from input_handlers import SimpleListInput logger = logging.getLogger(__name__) -# Client Commands -############################################################################# -# Connect Command class CodempConnectCommand(sublime_plugin.WindowCommand): - def is_enabled(self) -> bool: - return True def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride] def _(): @@ -65,10 +59,14 @@ class CodempDisconnectCommand(sublime_plugin.WindowCommand): return session.is_active() def run(self): - for ws in workspaces.lookup(): - ws.uninstall() + cli = session.client + 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 diff --git a/plugin/core/buffers.py b/plugin/core/buffers.py index 5b9ae7b..7df05b2 100644 --- a/plugin/core/buffers.py +++ b/plugin/core/buffers.py @@ -1,17 +1,23 @@ from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .workspace import WorkspaceManager + from ...lib import codemp import sublime import os import logging -from . import globals as g -from .utils import populate_view, safe_listener_attach, safe_listener_detach -import codemp +from .. import globals as g +from ..utils import populate_view +from ..utils import safe_listener_attach +from ..utils import safe_listener_detach +from ..utils import bidict logger = logging.getLogger(__name__) -def make_bufferchange_cb(buff: VirtualBuffer): - def __callback(bufctl: codemp.BufferController): +def bind_buffer_manager(buff: BufferManager): + def text_callback(bufctl: codemp.BufferController): def _(): change_id = buff.view.change_id() while change := bufctl.try_recv().wait(): @@ -41,70 +47,25 @@ def make_bufferchange_cb(buff: VirtualBuffer): "change_id": change_id, }, # pyright: ignore ) - sublime.set_timeout(_) - return __callback + return text_callback - -class VirtualBuffer: - def __init__( - self, - buffctl: codemp.BufferController, - view: sublime.View, - 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 +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 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) - def uninstall(self): - 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): + 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: @@ -114,6 +75,58 @@ class VirtualBuffer: 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() + 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() + + + + + + diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 60e18b4..443c62d 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -2,4 +2,4 @@ from .workspace import WorkspaceRegistry from .buffers import BufferRegistry workspaces = WorkspaceRegistry() -buffers = BufferRegistry() \ No newline at end of file +buffers = BufferRegistry() diff --git a/plugin/core/session.py b/plugin/core/session.py index 43532fd..5c6d290 100644 --- a/plugin/core/session.py +++ b/plugin/core/session.py @@ -9,10 +9,11 @@ class SessionManager(): self._driver = None self._client = None + def is_init(self): + return self._running and self._driver is not None + def is_active(self): - return self._driver is not None \ - and self._running \ - and self._client is not None + return self.is_init() and self._client is not None @property def client(self): @@ -37,23 +38,20 @@ class SessionManager(): if not self._driver: return - self.disconnect() + self.drop_client() self._driver.stop() self._running = False self._driver = None def connect(self, config: codemp.Config) -> codemp.Client: - if not self._running: + if not self.is_init(): self.get_or_init() self._client = codemp.connect(config).wait() logger.debug(f"Connected to '{config.host}' as user {self._client.user_name} (id: {self._client.user_id})") return self._client - def disconnect(self): - if not self._client: - return - + def drop_client(self): self._client = None diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index 17aa051..a3edc78 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -11,118 +11,16 @@ import tempfile import logging from .. import globals as g -from .buffers import VirtualBuffer from ..utils import draw_cursor_region from ..utils import bidict from ..core.registry import buffers 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 = ""): proj: dict = w.project_data() # pyright: ignore if proj is None: - proj = {"folders": []} # pyright: ignore, Value can be None + proj = {"folders": []} # pyright: ignore, `Value` can be None if name == "": entry = {"path": folder} @@ -147,35 +45,46 @@ def remove_project_folder(w: sublime.Window, filterstr: str): proj["folders"] = clean_proj_folders 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(): def __init__(self, handle: codemp.Workspace, window: sublime.Window, rootdir: str) -> None: self.handle: codemp.Workspace = handle self.window: sublime.Window = window self.curctl: codemp.CursorController = self.handle.cursor() self.rootdir: str = rootdir - - self.id = self.handle.id() + self.id: str = self.handle.id() def __del__(self): + logger.debug(f"dropping workspace {self.id}") self.curctl.clear_callback() self.curctl.stop() - # TODO: STUFF WITH THE BUFFERS IN THE REGISTRY - for buff in self.handle.buffer_list(): if not self.handle.detach(buff): logger.warning( 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]): # we can safely ignore the promise, we don't really care if everything # is ok for now with the cursor. self.curctl.send(id, start, end) - - class WorkspaceRegistry(): def __init__(self) -> None: self._workspaces: bidict[WorkspaceManager, sublime.Window] = bidict() @@ -189,7 +98,7 @@ class WorkspaceRegistry(): def lookupId(self, wid: str) -> WorkspaceManager | 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() tmpdir = tempfile.mkdtemp(prefix="codemp_") @@ -198,13 +107,13 @@ class WorkspaceRegistry(): wm = WorkspaceManager(wshandle, win, tmpdir) self._workspaces[wm] = win + return wm def remove(self, ws: WorkspaceManager | str | None): if isinstance(ws, str): ws = self.lookupId(ws) - if not ws: - return + if not ws: return remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}") shutil.rmtree(ws.rootdir, ignore_errors=True) diff --git a/plugin/globals.py b/plugin/globals.py index a0e0de5..2d11a97 100644 --- a/plugin/globals.py +++ b/plugin/globals.py @@ -1,16 +1,14 @@ BUFFCTL_TASK_PREFIX = "buffer-ctl" CURCTL_TASK_PREFIX = "cursor-ctl" -CODEMP_BUFFER_TAG = "codemp-buffer" -CODEMP_REMOTE_ID = "codemp-buffer-id" +CODEMP_VIEW_TAG = "codemp-buffer" +CODEMP_BUFFER_ID = "codemp-buffer-id" CODEMP_WORKSPACE_ID = "codemp-workspace-id" -CODEMP_WINDOW_TAG = "codemp-window" -CODEMP_WINDOW_WORKSPACES = "codemp-workspaces" - WORKSPACE_FOLDER_PREFIX = "CODEMP::" SUBLIME_REGIONS_PREFIX = "codemp-cursors" SUBLIME_STATUS_ID = "z_codemp_buffer" + CODEMP_IGNORE_NEXT_TEXT_CHANGE = "codemp-skip-change-event" ACTIVE_CODEMP_VIEW = None