From 8565356d38722e6c84b7063fb701578bd90b0bad Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 2 Nov 2024 18:26:37 +0100 Subject: [PATCH] chore: continuing refactor. should be almost done now. Cleaned up registries, commands and improved lookup logic. --- main.py | 73 ++++----- plugin/commands/client.py | 177 +++++++++++----------- plugin/commands/workspace.py | 281 ++++++++++++++++++----------------- plugin/core/buffers.py | 67 ++++++--- plugin/core/session.py | 13 +- plugin/core/workspace.py | 59 +++++--- plugin/input_handlers.py | 45 +++--- plugin/utils.py | 10 +- 8 files changed, 388 insertions(+), 337 deletions(-) diff --git a/main.py b/main.py index 016d28b..1924f10 100644 --- a/main.py +++ b/main.py @@ -73,11 +73,12 @@ class EventListener(sublime_plugin.EventListener): # client.driver.stop() def on_pre_close_window(self, window): - assert client.codemp is not None + assert session.client is not None - for vws in client.all_workspaces(window): - client.codemp.leave_workspace(vws.id) - client.uninstall_workspace(vws) + for vws in workspaces.lookup(window): + sublime.run_command("codemp_leave_workspace", { + "workspace_id": vws.id + }) def on_text_command(self, view, command_name, args): if command_name == "codemp_replace_text": @@ -91,7 +92,7 @@ class EventListener(sublime_plugin.EventListener): class CodempClientViewEventListener(sublime_plugin.ViewEventListener): @classmethod def is_applicable(cls, settings): - return settings.get(g.CODEMP_BUFFER_TAG) is not None + return settings.get(g.CODEMP_VIEW_TAG) is not None @classmethod def applies_to_primary_view_only(cls): @@ -102,14 +103,17 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): start = self.view.rowcol(region.begin()) end = self.view.rowcol(region.end()) - vws = client.workspace_from_view(self.view) - vbuff = client.buffer_from_view(self.view) - if vws is None or vbuff is None: - logger.error("we couldn't find the matching buffer or workspace!") + try: + _, vws, vbuff = objects_from_view(self.view) + except ValueError: + logger.error(f"Could not find buffers associated with the view {self.view}.\ + Removig the tag to disable event listener. Reattach.") + # delete the tag so we disable this event listener on the view + del self.view.settings()[g.CODEMP_VIEW_TAG] return - logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}") vws.send_cursor(vbuff.id, start, end) + logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}") def on_activated(self): global TEXT_LISTENER @@ -126,16 +130,12 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): logger.debug("closing active view") global TEXT_LISTENER safe_listener_detach(TEXT_LISTENER) # pyright: ignore - - vws = client.workspace_from_view(self.view) - vbuff = client.buffer_from_view(self.view) - if vws is None or vbuff is None: - logger.debug("no matching workspace or buffer.") + try: + _, vws, vbuff = objects_from_view(self.view) + buffers.remove(vbuff) + except ValueError: return - client.unregister_buffer(vbuff) - vws.uninstall_buffer(vbuff) - def on_text_command(self, command_name, args): if command_name == "codemp_replace_text": logger.info("got a codemp_replace_text command! but in the view listener") @@ -145,30 +145,16 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): logger.info("got a codemp_replace_text command! but in the view listener") -class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): - @classmethod - def is_applicable(cls, buffer): # pyright: ignore - # don't attach this event listener automatically - # we'll do it by hand with .attach(buffer). - return False - - def on_text_changed(self, changes): - s = self.buffer.primary_view().settings() - if s.get(g.CODEMP_IGNORE_NEXT_TEXT_CHANGE, False): - logger.debug("Ignoring echoing back the change.") - s[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = False - return - - vbuff = client.buffer_from_view(self.buffer.primary_view()) - if vbuff is not None: - logger.debug(f"local buffer change! {vbuff.id}") - vbuff.send_buffer_change(changes) - -TEXT_LISTENER = CodempClientTextChangeListener() - - - +# Next TODO: +# Server configurations: +# - where do we store it? +# - TOML? yes probably toml +# * Quickpanel for connecting with stuff. +# * Quickpanel for browsing the servers +# * Move all "server actions" like, create, delete, rename etc. as quickpanel actions. (See SFTP plugin.) +# * make panel for notifications! +# * make panel for errors and logging! # Proxy Commands ( NOT USED, left just in case we need it again. ) ############################################################################# @@ -184,3 +170,8 @@ TEXT_LISTENER = CodempClientTextChangeListener() # # def input_description(self): # return 'Share Buffer:' + + + + + diff --git a/plugin/commands/client.py b/plugin/commands/client.py index 52335b7..ad5f553 100644 --- a/plugin/commands/client.py +++ b/plugin/commands/client.py @@ -6,32 +6,13 @@ import random import codemp from ..core.session import session from ..core.workspace import workspaces -from ..core.buffers import buffers -from input_handlers import SimpleTextInput -from input_handlers import SimpleListInput +from ..input_handlers import SimpleTextInput +from ..input_handlers import SimpleListInput logger = logging.getLogger(__name__) class CodempConnectCommand(sublime_plugin.WindowCommand): - def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride] - def _(): - try: - config = codemp.Config( - username = user_name, - password = password, - host = server_host) - session.connect(config) - except Exception as e: - sublime.error_message( - "Could not connect:\n Make sure the server is up\n\ - and your credentials are correct." - ) - sublime.set_timeout_async(_) - - def input_description(self): - return "Server host:" - def input(self, args): if "server_host" not in args: return SimpleTextInput( @@ -51,6 +32,26 @@ class CodempConnectCommand(sublime_plugin.WindowCommand): ("password", "password?"), ) + def input_description(self): + return "Server host:" + + def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride] + def _(): + try: + config = codemp.Config( + username = user_name, + password = password, + host = server_host, + port=50053, + tls=False) + session.connect(config) + except Exception as e: + logger.error(e) + sublime.error_message( + "Could not connect:\n Make sure the server is up\n\ + and your credentials are correct." + ) + sublime.set_timeout_async(_) # Disconnect Command class CodempDisconnectCommand(sublime_plugin.WindowCommand): @@ -59,13 +60,13 @@ class CodempDisconnectCommand(sublime_plugin.WindowCommand): def run(self): cli = session.client - assert cli is not None for ws in workspaces.lookup(): if cli.leave_workspace(ws.id): workspaces.remove(ws) session.drop_client() + logger.info(f"disconnected from server '{session.config.host}'!") # Join Workspace Command @@ -73,117 +74,115 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand): def is_enabled(self) -> bool: return session.is_active() - def run(self, workspace_id): # pyright: ignore[reportIncompatibleMethodOverride] - if workspace_id is None: - return - - logger.info(f"Joining workspace: '{workspace_id}'...") - promise = session.client.join_workspace(workspace_id) - active_window = sublime.active_window() - - def _(): - try: - workspace = promise.wait() - except Exception as e: - logger.error( - f"Could not join workspace '{workspace_id}'.\n\nerror: {e}" - ) - sublime.error_message(f"Could not join workspace '{workspace_id}'") - return - workspaces.add(workspace) - sublime.set_timeout_async(_) - def input_description(self): return "Join:" def input(self, args): if "workspace_id" not in args: - list = session.client.list_workspaces(True, True) + wslist = session.get_workspaces() return SimpleListInput( - ("workspace_id", list.wait()), + ("workspace_id", wslist), ) + def run(self, workspace_id): # pyright: ignore[reportIncompatibleMethodOverride] + if workspace_id is None: + 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}'") + return + + logger.debug("Joined! Adding workspace to registry") + workspaces.add(ws) + # Leave Workspace Command class CodempLeaveWorkspaceCommand(sublime_plugin.WindowCommand): def is_enabled(self): - return client.codemp is not None and \ - len(client.all_workspaces(self.window)) > 0 - - def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride] - assert client.codemp is not None - if client.codemp.leave_workspace(workspace_id): - vws = client.workspace_from_id(workspace_id) - if vws is not None: - client.uninstall_workspace(vws) - else: - logger.error(f"could not leave the workspace '{workspace_id}'") + return session.is_active() and \ + len(workspaces.lookup(self.window)) > 0 def input(self, args): if "workspace_id" not in args: - return ActiveWorkspacesIdList() + wslist = session.client.active_workspaces() + return SimpleListInput( + ("workspace_id", wslist), + ) + + 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}'") class CodempInviteToWorkspaceCommand(sublime_plugin.WindowCommand): def is_enabled(self) -> bool: - return client.codemp is not None and len(client.all_workspaces(self.window)) > 0 - - def run(self, workspace_id: str, user: str): # pyright: ignore[reportIncompatibleMethodOverride] - assert client.codemp is not None - client.codemp.invite_to_workspace(workspace_id, user) - logger.debug(f"invite sent to user {user} for workspace {workspace_id}.") + return session.is_active() and len(workspaces.lookup(self.window)) > 0 def input(self, args): - assert client.codemp is not None if "workspace_id" not in args: - wslist = client.codemp.list_workspaces(True, False) + wslist = session.get_workspaces(owned=True, invited=False) return SimpleListInput( - ("workspace_id", wslist.wait()), ("user", "invitee's username") + ("workspace_id", wslist), ("user", "invitee's username") ) if "user" not in args: return SimpleTextInput(("user", "invitee's username")) + def run(self, workspace_id: str, user: str): # pyright: ignore[reportIncompatibleMethodOverride] + try: + session.client.invite_to_workspace(workspace_id, user) + logger.debug(f"invite sent to user {user} for workspace {workspace_id}.") + except Exception as e: + logger.error(f"Could not invite to workspace: {e}") + + class CodempCreateWorkspaceCommand(sublime_plugin.WindowCommand): def is_enabled(self): - return client.codemp is not None - - def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride] - assert client.codemp is not None - client.codemp.create_workspace(workspace_id) + return session.is_active() def input(self, args): if "workspace_id" not in args: - return SimpleTextInput(("workspace_id", "new workspace")) + return SimpleTextInput(("workspace_id", "new workspace name")) + + def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride] + try: + session.client.create_workspace(workspace_id) + except Exception as e: + logger.error(f"Could not create workspace: {e}") + class CodempDeleteWorkspaceCommand(sublime_plugin.WindowCommand): def is_enabled(self): - return client.codemp is not None + 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] - assert client.codemp is not None - - vws = client.workspace_from_id(workspace_id) - if vws is not None: + try: + vws = workspaces.lookupId(workspace_id) if not sublime.ok_cancel_dialog( "You are currently attached to '{workspace_id}'.\n\ Do you want to detach and delete it?", - ok_title="yes", - title="Delete Workspace?", + ok_title="yes", title="Delete Workspace?", ): return - if not client.codemp.leave_workspace(workspace_id): - logger.debug("error while leaving the workspace:") - raise RuntimeError("error while leaving the workspace") + self.window.run_command( + "codemp_leave_workspace", + {"workspace_id": workspace_id}) - client.uninstall_workspace(vws) + except KeyError: pass + finally: + session.client.delete_workspace(workspace_id) - client.codemp.delete_workspace(workspace_id) - - def input(self, args): - assert client.codemp is not None - workspaces = client.codemp.list_workspaces(True, False) # noqa: F841 - if "workspace_id" not in args: - return SimpleListInput(("workspace_id", workspaces.wait())) diff --git a/plugin/commands/workspace.py b/plugin/commands/workspace.py index 918aff6..289b07a 100644 --- a/plugin/commands/workspace.py +++ b/plugin/commands/workspace.py @@ -2,54 +2,74 @@ import sublime import sublime_plugin import logging -from .src.client import client -from listeners import TEXT_LISTENER -from input_handlers import SimpleTextInput -from input_handlers import ActiveWorkspacesIdList -from input_handlers import BufferIdList +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 +from ..input_handlers import SimpleListInput, SimpleTextInput logger = logging.getLogger(__name__) # Join Buffer Command class CodempJoinBufferCommand(sublime_plugin.WindowCommand): def is_enabled(self): - available_workspaces = client.all_workspaces(self.window) - return len(available_workspaces) > 0 + return len(workspaces.lookup(self.window)) > 0 + + def input_description(self) -> str: + return "Attach: " + + def input(self, args): + if "workspace_id" not in args: + wslist = session.get_workspaces(owned=True, invited=True) + return SimpleListInput( + ("workspace_id", wslist), + ) + + if "buffer_id" not in args: + try: ws = workspaces.lookupId(args["workspace_id"]) + except KeyError: + sublime.error_message("Workspace does not exists or is not active.") + return None + + bflist = ws.handle.fetch_buffers().wait() + return SimpleListInput( + ("buffer_id", bflist), + ) def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride] - # A workspace has some Buffers inside of it (filetree) - # some of those you are already attached to - # If already attached to it return the same alredy existing bufferctl - # if existing but not attached (attach) - # if not existing ask for creation (create + attach) - vws = client.workspace_from_id(workspace_id) - assert vws is not None + try: vws = workspaces.lookupId(workspace_id) + except KeyError: + logger.error(f"Can't create buffer: '{workspace_id}' does not exists or is not active.") + return - # is the buffer already installed? - if buffer_id in vws.codemp.buffer_list(): - logger.info("buffer already installed!") - return # do nothing. + try: # if it exists already, focus and listen + buff = buffers.lookupId(buffer_id) + safe_listener_detach(TEXT_LISTENER) + safe_listener_attach(TEXT_LISTENER, buff.view.buffer()) + self.window.focus_view(buff.view) + return + except KeyError: + pass - if buffer_id not in vws.codemp.filetree(filter=buffer_id): - create = sublime.ok_cancel_dialog( - "There is no buffer named '{buffer_id}' in the workspace '{workspace_id}'.\n\ + # if doesn't exist in the workspace, ask for creation. + if vws.handle.get_buffer(buffer_id) is None: + if sublime.ok_cancel_dialog( + f"There is no buffer named '{buffer_id}' in the workspace '{workspace_id}'.\n\ Do you want to create it?", - ok_title="yes", - title="Create Buffer?", - ) - if create: - try: - create_promise = vws.codemp.create(buffer_id) - except Exception as e: - logging.error(f"could not create buffer:\n\n {e}") - return - create_promise.wait() + ok_title="yes", title="Create Buffer?", + ): + sublime.run_command("codemp_create_buffer", { + "workspace_id": workspace_id, + "buffer_id": buffer_id + }) # now we can defer the attaching process logger.debug(f"attempting to attach to {buffer_id}...") - promise = vws.codemp.attach(buffer_id) + promise = vws.handle.attach_buffer(buffer_id) - def deferred_attach(promise): + def _(): try: buff_ctl = promise.wait() logger.debug("attach successfull!") @@ -58,151 +78,144 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand): sublime.error_message(f"Could not attach to buffer '{buffer_id}'") return - vbuff = vws.install_buffer(buff_ctl, TEXT_LISTENER) - client.register_buffer(vws, vbuff) # we need to keep track of it. + safe_listener_detach(TEXT_LISTENER) + vbuff = buffers.add(buff_ctl, vws) - # TODO! if the view is already active calling focus_view() - # will not trigger the on_activate - self.window.focus_view(vbuff.view) - - sublime.set_timeout_async(lambda: deferred_attach(promise)) - - def input_description(self) -> str: - return "Attach: " - - def input(self, args): - if "workspace_id" not in args: - return ActiveWorkspacesIdList(self.window, buffer_list=True) - - if "buffer_id" not in args: - return BufferIdList(args["workspace_id"]) + if self.window.active_view() == vbuff.view: + # if view is already active focusing it won't trigger `on_activate`. + safe_listener_attach(TEXT_LISTENER, vbuff.view.buffer()) + else: + self.window.focus_view(vbuff.view) + sublime.set_timeout_async(_) # Leave Buffer Comand class CodempLeaveBufferCommand(sublime_plugin.WindowCommand): def is_enabled(self): - return len(client.all_buffers()) > 0 - - def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride] - vbuff = client.buffer_from_id(buffer_id) - vws = client.workspace_from_id(workspace_id) - - if vbuff is None or vws is None: - sublime.error_message(f"You are not attached to the buffer '{id}'") - logging.warning(f"You are not attached to the buffer '{id}'") - return - - def defer_detach(): - if vws.codemp.detach(buffer_id): - vws.uninstall_buffer(vbuff) - client.unregister_buffer(vbuff) - - sublime.set_timeout_async(defer_detach) + return len(buffers.lookup()) > 0 def input_description(self) -> str: return "Leave: " def input(self, args): if "workspace_id" not in args: - return ActiveWorkspacesIdList(self.window, buffer_list=True) + wslist = session.client.active_workspaces() + return SimpleListInput( + ("workspace_id", wslist), + ) if "buffer_id" not in args: - return BufferIdList(args["workspace_id"]) + bflist = [bf.id for bf in buffers.lookup(args["workspace_id"])] + return SimpleListInput( + ("buffer_id", bflist) + ) + def run(self, workspace_id, buffer_id): # pyright: ignore[reportIncompatibleMethodOverride] + try: + buffers.lookupId(buffer_id) + vws = workspaces.lookupId(workspace_id) + except KeyError: + sublime.error_message(f"You are not attached to the buffer '{id}'") + logging.warning(f"You are not attached to the buffer '{id}'") + return + + if not vws.handle.get_buffer(buffer_id): + logging.error("The desired buffer is not managed by the workspace.") + return + + def _(): + try: + buffers.remove(buffer_id) + finally: + if not vws.handle.detach_buffer(buffer_id): + logger.error(f"could not leave the buffer {buffer_id}.") + sublime.set_timeout_async(_) # Leave Buffer Comand class CodempCreateBufferCommand(sublime_plugin.WindowCommand): def is_enabled(self): - return len(client.all_workspaces(self.window)) > 0 - - def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride] - vws = client.workspace_from_id(workspace_id) - - if vws is None: - sublime.error_message( - f"You are not attached to the workspace '{workspace_id}'" - ) - logging.warning(f"You are not attached to the workspace '{workspace_id}'") - return - - vws.codemp.create(buffer_id) - logging.info( - "created buffer '{buffer_id}' in the workspace '{workspace_id}'.\n\ - To interact with it you need to attach to it with Codemp: Attach." - ) + return len(workspaces.lookup()) > 0 def input_description(self) -> str: return "Create Buffer: " def input(self, args): if "workspace_id" not in args: - return ActiveWorkspacesIdList(self.window, buffer_text=True) + wslist = session.client.active_workspaces() + return SimpleListInput( + ("workspace_id", wslist), + ) if "buffer_id" not in args: return SimpleTextInput( - (("buffer_id", "new buffer")), + ("buffer_id", "new buffer name"), ) - -class CodempDeleteBufferCommand(sublime_plugin.WindowCommand): - def is_enabled(self): - return client.codemp is not None and len(client.codemp.active_workspaces()) > 0 - def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride] - vws = client.workspace_from_id(workspace_id) - if vws is None: + try: vws = workspaces.lookupId(workspace_id) + except KeyError: sublime.error_message( f"You are not attached to the workspace '{workspace_id}'" ) logging.warning(f"You are not attached to the workspace '{workspace_id}'") return - fetch_promise = vws.codemp.fetch_buffers() - delete = sublime.ok_cancel_dialog( - f"Confirm you want to delete the buffer '{buffer_id}'", - ok_title="delete", - title="Delete Buffer?", + vws.handle.create_buffer(buffer_id) + logging.info( + "created buffer '{buffer_id}' in the workspace '{workspace_id}'.\n\ + To interact with it you need to attach to it with Codemp: Attach." ) - if not delete: - return - fetch_promise.wait() - existing = vws.codemp.filetree(buffer_id) - if len(existing) == 0: - sublime.error_message( - f"The buffer '{buffer_id}' does not exists in the workspace." - ) - logging.info(f"The buffer '{buffer_id}' does not exists in the workspace.") - return - def deferred_delete(): - try: - vws.codemp.delete(buffer_id).wait() - except Exception as e: - logging.error( - f"error when deleting the buffer '{buffer_id}':\n\n {e}", True - ) - return - - vbuff = client.buffer_from_id(buffer_id) - if vbuff is None: - # we are not attached to it! - sublime.set_timeout_async(deferred_delete) - else: - if vws.codemp.detach(buffer_id): - vws.uninstall_buffer(vbuff) - sublime.set_timeout_async(deferred_delete) - else: - logging.error( - f"error while detaching from buffer '{buffer_id}', aborting the delete." - ) - return +class CodempDeleteBufferCommand(sublime_plugin.WindowCommand): + def is_enabled(self): + return len(workspaces.lookup()) > 0 def input_description(self) -> str: return "Delete buffer: " def input(self, args): if "workspace_id" not in args: - return ActiveWorkspacesIdList(self.window, buffer_list=True) + wslist = session.get_workspaces(owned=True, invited=False) + return SimpleListInput( + ("workspace_id", wslist), + ) if "buffer_id" not in args: - return BufferIdList(args["workspace_id"]) + try: ws = workspaces.lookupId(args["workspace_id"]) + except KeyError: + sublime.error_message("Workspace does not exists or is not attached.") + return sublime_plugin.BackInputHandler() + + bflist = ws.handle.fetch_buffers().wait() + return SimpleListInput( + ("buffer_id", bflist), + ) + + def run(self, workspace_id, buffer_id):# pyright: ignore[reportIncompatibleMethodOverride] + try: vws = workspaces.lookupId(workspace_id) + except KeyError: + sublime.error_message( + f"You are not attached to the workspace '{workspace_id}'" + ) + logging.warning(f"You are not attached to the workspace '{workspace_id}'") + return + + if not sublime.ok_cancel_dialog( + f"Confirm you want to delete the buffer '{buffer_id}'", + ok_title="delete", title="Delete Buffer?", + ): return + + try: + buffers.lookupId(buffer_id) + if not sublime.ok_cancel_dialog( + "You are currently attached to '{buffer_id}'.\n\ + Do you want to detach and delete it?", + ok_title="yes", title="Delete Buffer?", + ): + return + self.window.run_command( + "codemp_leave_buffer", + { "workspace_id": workspace_id, "buffer_id": buffer_id }) + except KeyError: pass + finally: + vws.handle.delete_buffer(buffer_id).wait() diff --git a/plugin/core/buffers.py b/plugin/core/buffers.py index 1da5d8b..4995293 100644 --- a/plugin/core/buffers.py +++ b/plugin/core/buffers.py @@ -8,6 +8,7 @@ import sublime import os import logging +from codemp import TextChange from .. import globals as g from ..utils import populate_view from ..utils import safe_listener_attach @@ -20,33 +21,40 @@ def bind_callback(v: sublime.View): def _callback(bufctl: codemp.BufferController): def _(): change_id = v.change_id() - while change := bufctl.try_recv().wait(): + while buffup := bufctl.try_recv().wait(): logger.debug("received remote buffer change!") - if change is None: + if buffup is None: break - if change.is_empty(): + 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.id() == g.ACTIVE_CODEMP_VIEW: + 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. - v.run_command( - "codemp_replace_text", - { - "start": change.start, - "end": change.end, - "content": change.content, - "change_id": change_id, - }, # pyright: ignore - ) + try: + change = buffup.change + v.run_command( + "codemp_replace_text", + { + "start": change.start, + "end": change.end, + "content": change.content, + "change_id": change_id, + }, # pyright: ignore + ) + except Exception as e: + raise e + + bufctl.ack(buffup.version) sublime.set_timeout(_) return _callback @@ -61,7 +69,6 @@ class BufferManager(): def __del__(self): logger.debug(f"dropping buffer {self.id}") self.handle.clear_callback() - self.handle.stop() def __hash__(self): return hash(self.id) @@ -76,8 +83,9 @@ class BufferManager(): 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(region.begin(), region.end(), change.str).wait() + self.handle.send(TextChange(start=region.begin(), end=region.end(), content=change.str)) def sync(self, text_listener): promise = self.handle.content() @@ -95,32 +103,43 @@ class BufferRegistry(): def lookup(self, ws: Optional[WorkspaceManager] = None) -> list[BufferManager]: if not ws: return list(self._buffers.keys()) - bf = self._buffers.inverse.get(ws) - return bf if bf else [] + bfs = self._buffers.inverse.get(ws) + return bfs if bfs else [] - def lookupId(self, bid: str) -> Optional[BufferManager]: - return next((bf for bf in self._buffers if bf.id == bid), None) + 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 add(self, bhandle: codemp.BufferController, wsm: WorkspaceManager): bid = bhandle.path() - tmpfile = os.path.join(wsm.rootdir, bid) - open(tmpfile, "a").close() + # tmpfile = os.path.join(wsm.rootdir, bid) + # open(tmpfile, "a").close() + content = bhandle.content() win = sublime.active_window() view = win.open_file(bid) view.set_scratch(True) - view.retarget(tmpfile) + # 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]") + populate_view(view, content.wait()) + tmpfile = "DISABLE" bfm = BufferManager(bhandle, view, tmpfile) self._buffers[bfm] = wsm - def remove(self, bf: Optional[BufferManager | str]): + return bfm + + def remove(self, bf: BufferManager | str): if isinstance(bf, str): bf = self.lookupId(bf) - if not bf: return del self._buffers[bf] bf.view.close() diff --git a/plugin/core/session.py b/plugin/core/session.py index 82f5f33..305a4ed 100644 --- a/plugin/core/session.py +++ b/plugin/core/session.py @@ -1,6 +1,8 @@ import logging import codemp +from ..utils import some + logger = logging.getLogger(__name__) class SessionManager(): @@ -17,7 +19,7 @@ class SessionManager(): @property def client(self): - return self._client + return some(self._client) def get_or_init(self) -> codemp.Driver: if self._driver: @@ -48,9 +50,16 @@ class SessionManager(): 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})") + self.config = config + logger.debug(f"Connected to '{self.config.host}' as user {self._client.current_user().name} (id: {self._client.current_user().id})") return self._client + def get_workspaces(self, owned: bool = True, invited: bool = True): + owned_wss = self.client.fetch_owned_workspaces().wait() if owned else [] + invited_wss = self.client.fetch_joined_workspaces().wait() if invited else [] + + return owned_wss + invited_wss + def drop_client(self): self._client = None diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index 7c907b1..cc213b6 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -2,14 +2,15 @@ from __future__ import annotations from typing import Optional, Tuple from typing import TYPE_CHECKING if TYPE_CHECKING: - from ...main import CodempClientTextChangeListener import codemp import sublime import shutil import tempfile import logging +import gc +from codemp import Selection from .. import globals as g from ..utils import draw_cursor_region from ..utils import bidict @@ -18,9 +19,9 @@ from .buffers import buffers logger = logging.getLogger(__name__) 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 = w.project_data() + if not isinstance(proj, dict): + proj = {"folders": []} if name == "": entry = {"path": folder} @@ -32,7 +33,7 @@ def add_project_folder(w: sublime.Window, folder: str, name: str = ""): w.set_project_data(proj) def remove_project_folder(w: sublime.Window, filterstr: str): - proj: dict = self.window.project_data() # type:ignore + proj: dict = w.project_data() # type:ignore if proj is None: return @@ -51,10 +52,13 @@ def cursor_callback(ctl: codemp.CursorController): while event := ctl.try_recv().wait(): if event is None: break - bfm = buffers.lookupId(event.buffer) - if not bfm: continue + try: bfm = buffers.lookupId(event.sel.buffer) + except KeyError: continue - draw_cursor_region(bfm.view, event.start, event.end, event.user) + region_start = (event.sel.start_row, event.sel.start_col) + region_end = (event.sel.end_row, event.sel.end_col) + + draw_cursor_region(bfm.view, region_start, region_end, event.user) sublime.set_timeout_async(_) class WorkspaceManager(): @@ -70,8 +74,8 @@ class WorkspaceManager(): logger.debug(f"dropping workspace {self.id}") self.curctl.clear_callback() - for buff in self.handle.buffer_list(): - if not self.handle.detach(buff): + for buff in self.handle.active_buffers(): + if not self.handle.detach_buffer(buff): logger.warning( f"could not detach from '{buff}' for workspace '{self.id}'." ) @@ -82,7 +86,14 @@ class WorkspaceManager(): 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) + sel = Selection( + start_row=start[0], + start_col=start[1], + end_row=end[0], + end_col=end[1], + buffer=id + ) + self.curctl.send(sel) class WorkspaceRegistry(): def __init__(self) -> None: @@ -94,31 +105,37 @@ class WorkspaceRegistry(): ws = self._workspaces.inverse.get(w) return ws if ws else [] - def lookupId(self, wid: str) -> Optional[WorkspaceManager]: - return next((ws for ws in self._workspaces if ws.id == wid), None) + def lookupParent(self, ws: WorkspaceManager | str) -> sublime.Window: + if isinstance(ws, str): + wsm = self.lookupId(ws) + return self._workspaces[ws] + + def lookupId(self, wid: str) -> WorkspaceManager: + wsm = next((ws for ws in self._workspaces if ws.id == wid), None) + if not wsm: raise KeyError + return wsm def add(self, wshandle: codemp.Workspace) -> WorkspaceManager: win = sublime.active_window() - tmpdir = tempfile.mkdtemp(prefix="codemp_") - name = f"{g.WORKSPACE_FOLDER_PREFIX}{wshandle.id()}" - add_project_folder(win, tmpdir, name) + # tmpdir = tempfile.mkdtemp(prefix="codemp_") + # add_project_folder(win, tmpdir, f"{g.WORKSPACE_FOLDER_PREFIX}{wshandle.id()}") + tmpdir = "DISABLED" wm = WorkspaceManager(wshandle, win, tmpdir) self._workspaces[wm] = win return wm - def remove(self, ws: Optional[WorkspaceManager | str]): + def remove(self, ws: WorkspaceManager | str): if isinstance(ws, str): ws = self.lookupId(ws) - if not ws: return - - remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}") - shutil.rmtree(ws.rootdir, ignore_errors=True) + # remove_project_folder(ws.window, f"{g.WORKSPACE_FOLDER_PREFIX}{ws.id}") + # shutil.rmtree(ws.rootdir, ignore_errors=True) del self._workspaces[ws] + workspaces = WorkspaceRegistry() diff --git a/plugin/input_handlers.py b/plugin/input_handlers.py index 5beb604..0ac57f8 100644 --- a/plugin/input_handlers.py +++ b/plugin/input_handlers.py @@ -7,10 +7,9 @@ from typing import Tuple, Union, List ############################################################ class SimpleTextInput(sublime_plugin.TextInputHandler): def __init__(self, *args: Tuple[str, Union[str, List[str]]]): - logging.debug(f"why isn't the text input working? {args}") - self.argname = args[0][0] - self.default = args[0][1] - self.next_inputs = args[1:] + self.input, *self.next_inputs = args + self.argname = self.input[0] + self.default = self.input[1] def initial_text(self): if isinstance(self.default, str): @@ -32,9 +31,9 @@ class SimpleTextInput(sublime_plugin.TextInputHandler): class SimpleListInput(sublime_plugin.ListInputHandler): def __init__(self, *args: Tuple[str, Union["list[str]", str]]): - self.argname = args[0][0] - self.list = args[0][1] - self.next_inputs = args[1:] + self.input, *self.next_inputs = args + self.argname = self.input[0] + self.list = self.input[1] def name(self): return self.argname @@ -133,23 +132,23 @@ class SimpleListInput(sublime_plugin.ListInputHandler): # return AddListEntry(self) -class AddListEntry(sublime_plugin.TextInputHandler): - # this class works when the list input handler - # added appended a new element to it's list that will need to be - # replaced with the entry added from here! - def __init__(self, list_input_handler): - self.parent = list_input_handler +# class AddListEntry(sublime_plugin.TextInputHandler): +# # this class works when the list input handler +# # added appended a new element to it's list that will need to be +# # replaced with the entry added from here! +# def __init__(self, list_input_handler): +# self.parent = list_input_handler - def name(self): - return "" +# def name(self): +# return "" - def validate(self, text: str) -> bool: - return not len(text) == 0 +# def validate(self, text: str) -> bool: +# return not len(text) == 0 - def confirm(self, text: str): - self.parent.list.pop() # removes the add_entry_text - self.parent.list.insert(0, text) - self.parent.preselected = 0 +# def confirm(self, text: str): +# self.parent.list.pop() # removes the add_entry_text +# self.parent.list.insert(0, text) +# self.parent.preselected = 0 - def next_input(self, args): - return sublime_plugin.BackInputHandler() +# def next_input(self, args): +# return sublime_plugin.BackInputHandler() diff --git a/plugin/utils.py b/plugin/utils.py index 6f9b7c4..16e9a72 100644 --- a/plugin/utils.py +++ b/plugin/utils.py @@ -1,6 +1,6 @@ import sublime import sublime_plugin -from typing import Dict, Generic, TypeVar +from typing import Dict, Generic, TypeVar, Optional from . import globals as g # bidirectional dictionary so that we can have bidirectional @@ -13,7 +13,6 @@ D = TypeVar("D", Dict, dict) K = TypeVar("K") V = TypeVar("V") - # using del bd.inverse[key] doesn't work since it can't be intercepted. # the only way is to iterate: # for key in bd.inverse[inverse_key] @@ -94,7 +93,7 @@ def populate_view(view, content): ) -def get_view_from_local_path(path): +def view_from_local_path(path): for window in sublime.windows(): for view in window.views(): if view.file_name() == path: @@ -115,3 +114,8 @@ def draw_cursor_region(view, start, end, user): annotations=[user], # pyright: ignore annotation_color=g.PALETTE[user_hash % len(g.PALETTE)], ) + +T = TypeVar("T") +def some(x: Optional[T]) -> T: + assert x is not None + return x