From e7a3f7f3676967246161685f8d9cd81989539a67 Mon Sep 17 00:00:00 2001
From: cschen <camillo.schenone@gmail.com>
Date: Sat, 22 Feb 2025 19:39:32 +0100
Subject: [PATCH] fix: clearer creation of virtual workspaces and buffers. fix:
 fix messages formatting fix: handle workspaces without buffers when attaching
 fix: workspave lookupparent feat: initial setup to support local views

---
 plugin/commands/client.py    | 42 ++++++--------------
 plugin/commands/workspace.py | 55 +++++++++++----------------
 plugin/core/buffers.py       | 74 +++++++++++++++++++++++++-----------
 plugin/core/workspace.py     | 32 ++++++++++++----
 4 files changed, 110 insertions(+), 93 deletions(-)

diff --git a/plugin/commands/client.py b/plugin/commands/client.py
index 16714fc..9158ee7 100644
--- a/plugin/commands/client.py
+++ b/plugin/commands/client.py
@@ -119,11 +119,9 @@ class CodempDisconnectCommand(sublime_plugin.WindowCommand):
         return session.is_active()
 
     def run(self):
-        cli = session.client
-
-        for ws in workspaces.lookup():
-            if cli.leave_workspace(ws.id):
-                workspaces.remove(ws)
+        wslist = session.client.active_workspaces()
+        for ws in wslist:
+            workspaces.remove(ws)
 
         session.drop_client()
         logger.info(f"disconnected from server '{session.config.host}'!")
@@ -145,19 +143,11 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand):
             )
 
     def run(self, workspace_id):  # pyright: ignore[reportIncompatibleMethodOverride]
-        if workspace_id is None:
+        if workspace_id in workspaces:
             return
 
         logger.info(f"Joining workspace: '{workspace_id}'...")
-        try:
-            ws = session.client.attach_workspace(workspace_id).wait()
-        except Exception as e:
-            logger.error(f"Could not join workspace '{workspace_id}': {e}")
-            sublime.error_message(f"Could not join workspace '{workspace_id}'")
-            raise e
-
-        logger.debug("Joined! Adding workspace to registry")
-        workspaces.register(ws)
+        workspaces.register(workspace_id)
 
 
 # Leave Workspace Command
@@ -172,14 +162,13 @@ class CodempLeaveWorkspaceCommand(sublime_plugin.WindowCommand):
             )
 
     def run(self, workspace_id: str):  # pyright: ignore[reportIncompatibleMethodOverride]
-        try:
-            workspaces.remove(workspace_id)
-        finally:
-            if not session.client.leave_workspace(workspace_id):
-                logger.error(f"could not leave the workspace '{workspace_id}'")
-            else:
-                logger.debug(f"successfully left the workspace '{workspace_id}'")
+        if workspace_id not in workspaces:
+            sublime.error_message(f"You are not attached to the workspace '{workspace_id}'")
+            logger.warning(f"You are not attached to the workspace_id '{workspace_id}'")
+            return
 
+        logger.debug("We are about to remove the workspace {}".format(workspace_id))
+        workspaces.remove(workspace_id)
 
 class CodempInviteToWorkspaceCommand(sublime_plugin.WindowCommand):
     def is_enabled(self) -> bool:
@@ -208,10 +197,6 @@ class CodempCreateWorkspaceCommand(sublime_plugin.WindowCommand):
     def is_enabled(self):
         return session.is_active()
 
-    # def input(self, args):
-    #     if "workspace_id" not in args:
-    #         return SimpleTextInput(("workspace_id", "new workspace name"))
-
     def run(self, workspace_id: str):  # pyright: ignore[reportIncompatibleMethodOverride]
         try:
             session.client.create_workspace(workspace_id)
@@ -224,11 +209,6 @@ class CodempDeleteWorkspaceCommand(sublime_plugin.WindowCommand):
     def is_enabled(self):
         return session.is_active()
 
-    # def input(self, args):
-    #     workspaces = session.get_workspaces(owned=True, invited=False)  # noqa: F841
-    #     if "workspace_id" not in args:
-    #         return SimpleListInput(("workspace_id", workspaces))
-
     def run(self, workspace_id: str):  # pyright: ignore[reportIncompatibleMethodOverride]
         if workspace_id in workspaces:
             if not sublime.ok_cancel_dialog(
diff --git a/plugin/commands/workspace.py b/plugin/commands/workspace.py
index 1cafb2e..dfc94d9 100644
--- a/plugin/commands/workspace.py
+++ b/plugin/commands/workspace.py
@@ -1,14 +1,14 @@
 import sublime
 import sublime_plugin
 import logging
-import gc
+import os
 
 from ..core.session import session
 from ..core.workspace import workspaces
 from ..core.buffers import buffers
 
 from ..text_listener import TEXT_LISTENER
-from ..utils import safe_listener_attach, safe_listener_detach, populate_view
+from ..utils import some
 from ..input_handlers import SimpleListInput, SimpleTextInput
 
 logger = logging.getLogger(__name__)
@@ -37,11 +37,18 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
                         return None
 
                     bflist = ws.handle.fetch_buffers().wait()
-                    return SimpleListInput(
-                        ("buffer_id", bflist),
-                    )
+                    if bflist:
+                        return SimpleListInput(
+                            ("buffer_id", bflist)
+                        )
+                    else:
+                        sublime.error_message("Workspace does not have any buffers inside.")
+                        return None
 
     def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride]
+        if not buffer_id:
+            return
+
         try: vws = workspaces.lookupId(workspace_id)
         except KeyError:
             logger.error(f"Can't create buffer: '{workspace_id}' does not exists or is not active.")
@@ -54,25 +61,14 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand):
         except KeyError:
             pass
 
-        # now we can defer the attaching process
         logger.debug(f"attempting to attach to {buffer_id}...")
-        ctl_promise = vws.handle.attach_buffer(buffer_id)
 
         def _():
-            try:
-                buff_ctl = ctl_promise.wait()
-                logger.debug("attach successfull!")
-            except Exception as e:
-                logger.error(f"error when attaching to buffer '{id}':\n\n {e}")
-                sublime.error_message(f"Could not attach to buffer '{buffer_id}'")
-                return
-
-            vbuff = buffers.register(buff_ctl, vws)
+            vbuff = some(buffers.register(buffer_id, vws))
             vbuff.sync(TEXT_LISTENER)
 
         sublime.set_timeout_async(_)
 
-
 # Leave Buffer Comand
 class CodempLeaveBufferCommand(sublime_plugin.WindowCommand):
     def is_enabled(self):
@@ -88,20 +84,13 @@ class CodempLeaveBufferCommand(sublime_plugin.WindowCommand):
             )
 
     def run(self, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride]
-        try:
-            buff = buffers.lookupId(buffer_id)
-            vws = buffers.lookupParent(buff)
-        except KeyError:
-            sublime.error_message(f"You are not attached to the buffer '{buffer_id}'")
-            logger.warning(f"You are not attached to the buffer '{buffer_id}'")
-            return
-
-        if not vws.handle.get_buffer(buffer_id):
-            logger.error("The desired buffer is not managed by the workspace.")
+        if buffer_id not in buffers:
+            logger.warning(f"The buffer was already removed:  '{buffer_id}'")
             return
 
         # The call must happen separately, otherwise it causes sublime to crash...
         # no idea why...
+        sublime.set_timeout(lambda: buffers.remove(buffer_id), 10)
         def _():
             buffers.remove(buffer_id)
             if not vws.handle.detach_buffer(buffer_id):
@@ -113,7 +102,7 @@ class CodempLeaveBufferCommand(sublime_plugin.WindowCommand):
 # Leave Buffer Comand
 class CodempCreateBufferCommand(sublime_plugin.WindowCommand):
     def is_enabled(self):
-        return len(workspaces.lookup()) > 0
+        return workspaces.hasactive()
 
     def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride]
         try: vws = workspaces.lookupId(workspace_id)
@@ -122,9 +111,9 @@ class CodempCreateBufferCommand(sublime_plugin.WindowCommand):
             logger.warning(f"You are not attached to the workspace '{workspace_id}'")
             return
 
-        vws.handle.create_buffer(buffer_id)
+        vws.handle.create_buffer(buffer_id).wait()
         logger.info(
-            "created buffer '{buffer_id}' in the workspace '{workspace_id}'.\n\
+            f"created buffer '{buffer_id}' in the workspace '{workspace_id}'.\n\
             To interact with it you need to attach to it with Codemp: Attach."
         )
 
@@ -136,14 +125,14 @@ class CodempDeleteBufferCommand(sublime_plugin.WindowCommand):
 
         try: vws = workspaces.lookupId(workspace_id)
         except KeyError:
-            sublime.error_message(f"You are not attached to the workspace '{workspace_id}'")
-            logger.warning(f"You are not attached to the workspace '{workspace_id}'")
+            sublime.error_message(f"You are not attached to the workspace {workspace_id}")
+            logger.warning(f"You are not attached to the workspace {workspace_id}")
             return
 
         if buffer_id in buffers:
 
             if not sublime.ok_cancel_dialog(
-                "You are currently attached to '{buffer_id}'.\n\
+                f"You are currently attached to '{buffer_id}'.\n\
                 Do you want to detach and delete it?",
                 ok_title="yes", title="Delete Buffer?",
             ): return
diff --git a/plugin/core/buffers.py b/plugin/core/buffers.py
index b535a80..e0a552f 100644
--- a/plugin/core/buffers.py
+++ b/plugin/core/buffers.py
@@ -71,17 +71,27 @@ def bind_callback(v: sublime.View):
     return _callback
 
 class BufferManager():
-    def __init__(self, handle: codemp.BufferController, v: sublime.View, filename: str):
+    def __init__(self, handle: codemp.BufferController, v: sublime.View, filename: str, islocal: bool):
         self.handle: codemp.BufferController = handle
         self.view: sublime.View = v
+        self.islocal: bool = islocal
         self.id = self.handle.path()
         self.filename = filename
         self.handle.callback(bind_callback(self.view))
 
+        self.view.settings().set(g.CODEMP_VIEW_TAG, True)
+        self.view.settings().set(g.CODEMP_BUFFER_ID, self.id)
+        self.view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]")
+
     def __del__(self):
         logger.debug(f"dropping buffer {self.id}")
-        self.view.close()
         self.handle.clear_callback()
+        if self.islocal:
+            self.view.settings().erase(g.CODEMP_BUFFER_ID)
+            self.view.settings().erase(g.CODEMP_VIEW_TAG)
+            self.view.set_status(g.SUBLIME_STATUS_ID, "")
+        else:
+            self.view.close()
 
     def __hash__(self):
         return hash(self.id)
@@ -103,17 +113,26 @@ class BufferManager():
     def sync(self, text_listener):
         promise = self.handle.content()
         def _():
-            current_contents = get_contents(self.view)
+            # 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(_)
 
+    def overwrite(self, text_listener):
+        localcontents = get_contents(self.view)
+        remotecontents = self.handle.content().wait()
+        remotelen = len(remotecontents)
+        self.handle.send(
+            TextChange(start=0, end=remotelen, content= localcontents)
+        )
+        self.sync(text_listener)
+
+
 class BufferRegistry():
     def __init__(self):
         self._buffers: bidict[BufferManager, WorkspaceManager] = bidict()
@@ -142,28 +161,32 @@ class BufferRegistry():
         if not bfm: raise KeyError
         return bfm
 
-    def register(self, bhandle: codemp.BufferController, wsm: WorkspaceManager):
-        bid = bhandle.path()
-    
+    def register(self, buff: str, wsm: WorkspaceManager, localview: sublime.View | None = None):
+
+        try: buffctl = wsm.handle.attach_buffer(buff).wait()
+        except Exception as e:
+            logger.error(f"error when attaching to buffer '{id}':\n\n {e}")
+            sublime.error_message(f"Could not attach to buffer '{buff}'")
+            raise e
+
         win = sublime.active_window()
-        newfileflags = sublime.NewFileFlags.TRANSIENT \
-            | sublime.NewFileFlags.ADD_TO_SELECTION \
-            | sublime.NewFileFlags.FORCE_CLONE
-        view = win.new_file(newfileflags)
+        if not localview:
+            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]")
+            view.set_scratch(True)
+            view.set_name(os.path.basename(buff))
+            syntax = sublime.find_syntax_for_file(buff)
+            if syntax:
+                view.assign_syntax(syntax)
+        else:
+            view = localview
 
         tmpfile = "DISABLE"
-        bfm = BufferManager(bhandle, view, tmpfile)
+        bfm = BufferManager(buffctl, view, tmpfile, islocal = localview is not None)
         self._buffers[bfm] = wsm
 
         return bfm
@@ -171,9 +194,16 @@ class BufferRegistry():
     def remove(self, bf: BufferManager | str):
         if isinstance(bf, str):
             bf = self.lookupId(bf)
+        ws = self.lookupParent(bf)
 
         del self._buffers[bf]
 
+        bf = bf.id
+        if not ws.handle.detach_buffer(bf):
+            logger.error(f"could not leave the buffer {bf}.")
+        else:
+            logger.debug(f"successfully detached from {bf}.")
+
 
 buffers = BufferRegistry()
 
diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py
index 810c2c4..b177d6b 100644
--- a/plugin/core/workspace.py
+++ b/plugin/core/workspace.py
@@ -8,7 +8,6 @@ import sublime
 import shutil
 import tempfile
 import logging
-import gc
 
 from codemp import Selection
 from .. import globals as g
@@ -107,7 +106,7 @@ class WorkspaceRegistry():
         return True
 
     def hasactive(self):
-        return len(session.client.active_workspaces()) > 0
+        return len(self._workspaces.keys()) > 0
 
     def lookup(self, w: Optional[sublime.Window] = None) -> list[WorkspaceManager]:
         if not w:
@@ -117,7 +116,7 @@ class WorkspaceRegistry():
 
     def lookupParent(self, ws: WorkspaceManager | str) -> sublime.Window:
         if isinstance(ws, str):
-            wsm = self.lookupId(ws)
+            ws = self.lookupId(ws)
         return self._workspaces[ws]
 
     def lookupId(self, wid: str) -> WorkspaceManager:
@@ -125,25 +124,44 @@ class WorkspaceRegistry():
         if not wsm: raise KeyError
         return wsm
 
-    def register(self, wshandle: codemp.Workspace) -> WorkspaceManager:
+    def register(self, wid: str) -> WorkspaceManager:
         win = sublime.active_window()
 
         # tmpdir = tempfile.mkdtemp(prefix="codemp_")
         # add_project_folder(win, tmpdir, f"{g.WORKSPACE_FOLDER_PREFIX}{wshandle.id()}")
+        try:
+            ws = session.client.attach_workspace(wid).wait()
+        except Exception as e:
+            logger.error(f"Could not join workspace '{wid}': {e}")
+            sublime.error_message(f"Could not join workspace '{wid}'")
+            raise e
+        logger.debug("Joined! Adding workspace to registry")
 
         tmpdir = "DISABLED"
-        wm = WorkspaceManager(wshandle, win, tmpdir)
+        wm = WorkspaceManager(ws, win, tmpdir)
         self._workspaces[wm] = win
+
         return wm
 
     def remove(self, ws: WorkspaceManager | str):
         if isinstance(ws, str):
             ws = self.lookupId(ws)
 
-        # remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}")
-        # shutil.rmtree(ws.rootdir, ignore_errors=True)
+        logger.debug("removing all the buffers from the workspace")
+        # we need ids, we can't keep references to the buffermanager here
+        bufferlist = [buff.id for buff in buffers.lookup(ws)]
+        for buff in bufferlist:
+            logger.debug("removing the buffer {}".format(buff))
+            buffers.remove(buff)
+
         del self._workspaces[ws]
 
+        ws = ws.id
+        if not session.client.leave_workspace(ws):
+            logger.error(f"could not leave the workspace '{ws}'")
+        else:
+            logger.debug(f"successfully left the workspace '{ws}'")
+
 
 
 workspaces = WorkspaceRegistry()