From a8cc08962436e9614fb823bbe4b2a7cf41156edc Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 17 Sep 2024 22:20:00 +0200 Subject: [PATCH 01/13] wip: huge refactor with smarter and simpler struture Former-commit-id: fbd0bca8094642dd8d2cf6c3f154af3b10dff95b --- .gitignore | 2 +- input_handlers.py | 159 ------------- listeners.py => main.py | 91 +++++++- plugin.py | 72 ------ .../commands/client.py | 29 ++- .../commands/workspace.py | 0 {src => plugin/core}/buffers.py | 0 {src => plugin/core}/client.py | 68 +----- plugin/core/registry.py | 5 + plugin/core/session.py | 60 +++++ plugin/core/workspace.py | 221 ++++++++++++++++++ {src => plugin}/globals.py | 0 plugin/input_handlers.py | 155 ++++++++++++ {src => plugin}/utils.py | 0 src/workspace.py | 133 ----------- 15 files changed, 545 insertions(+), 450 deletions(-) delete mode 100644 input_handlers.py rename listeners.py => main.py (57%) delete mode 100644 plugin.py rename client_commands.py => plugin/commands/client.py (90%) rename workspace_commands.py => plugin/commands/workspace.py (100%) rename {src => plugin/core}/buffers.py (100%) rename {src => plugin/core}/client.py (56%) create mode 100644 plugin/core/registry.py create mode 100644 plugin/core/session.py create mode 100644 plugin/core/workspace.py rename {src => plugin}/globals.py (100%) create mode 100644 plugin/input_handlers.py rename {src => plugin}/utils.py (100%) diff --git a/.gitignore b/.gitignore index ea7702e..325ea16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /target test* -/lib +lib # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/input_handlers.py b/input_handlers.py deleted file mode 100644 index 3f84e76..0000000 --- a/input_handlers.py +++ /dev/null @@ -1,159 +0,0 @@ -import sublime_plugin -import logging - -from typing import Tuple, Union, List - -from .src.client import client - -logger = logging.getLogger(__name__) - - -# Input handlers -############################################################ -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:] - - def initial_text(self): - if isinstance(self.default, str): - return self.default - else: return "" - - def name(self): - return self.argname - - def next_input(self, args): - if len(self.next_inputs) > 0: - if self.next_inputs[0][0] not in args: - if isinstance(self.next_inputs[0][1], list): - return SimpleListInput(*self.next_inputs) - else: - return SimpleTextInput(*self.next_inputs) - - -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:] - - def name(self): - return self.argname - - def list_items(self): - if isinstance(self.list, list): - return self.list - else: - return [self.list] - - def next_input(self, args): - if len(self.next_inputs) > 0: - if self.next_inputs[0][0] not in args: - if isinstance(self.next_inputs[0][1], str): - return SimpleTextInput(*self.next_inputs) - else: - return SimpleListInput(*self.next_inputs) - - -class ActiveWorkspacesIdList(sublime_plugin.ListInputHandler): - def __init__(self, window=None, buffer_list=False, buffer_text=False): - self.window = window - self.buffer_list = buffer_list - self.buffer_text = buffer_text - - def name(self): - return "workspace_id" - - def list_items(self): - return [vws.id for vws in client.all_workspaces(self.window)] - - def next_input(self, args): - if self.buffer_list: - return BufferIdList(args["workspace_id"]) - elif self.buffer_text: - return SimpleTextInput(("buffer_id", "new buffer")) - - -# To allow for having a selection and choosing non existing workspaces -# we do a little dance: We pass this list input handler to a TextInputHandler -# when we select "Create New..." which adds his result to the list of possible -# workspaces and pop itself off the stack to go back to the list handler. -class WorkspaceIdList(sublime_plugin.ListInputHandler): - def __init__(self): - assert client.codemp is not None # the command should not be available - - # at the moment, the client can't give us a full list of existing workspaces - # so a textinputhandler would be more appropriate. but we keep this for the future - - self.add_entry_text = "* add entry..." - self.list = client.codemp.list_workspaces(True, True).wait() - self.list.sort() - self.list.append(self.add_entry_text) - self.preselected = None - - def name(self): - return "workspace_id" - - def placeholder(self): - return "Workspace" - - def list_items(self): - if self.preselected is not None: - return (self.list, self.preselected) - else: - return self.list - - def next_input(self, args): - if args["workspace_id"] == self.add_entry_text: - return AddListEntry(self) - - -class BufferIdList(sublime_plugin.ListInputHandler): - def __init__(self, workspace_id): - vws = client.workspace_from_id(workspace_id) - self.add_entry_text = "* create new..." - self.list = vws.codemp.filetree(None) - self.list.sort() - self.list.append(self.add_entry_text) - self.preselected = None - - def name(self): - return "buffer_id" - - def placeholder(self): - return "Buffer Id" - - def list_items(self): - if self.preselected is not None: - return (self.list, self.preselected) - else: - return self.list - - def next_input(self, args): - if args["buffer_id"] == self.add_entry_text: - 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 - - def name(self): - return "" - - 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 next_input(self, args): - return sublime_plugin.BackInputHandler() diff --git a/listeners.py b/main.py similarity index 57% rename from listeners.py rename to main.py index 21cf166..fd8432a 100644 --- a/listeners.py +++ b/main.py @@ -1,25 +1,76 @@ +# pyright: reportIncompatibleMethodOverride=false import sublime import sublime_plugin import logging -from .src.client import client -from .src.utils import safe_listener_attach -from .src.utils import safe_listener_detach -from .src import globals as g +from lib import codemp +from .plugin.utils import safe_listener_detach +from .plugin.core.session import session +from .plugin.core.registry import workspaces +from .plugin.core.registry import buffers +from .plugin.commands.client import CodempConnectCommand +from .plugin.commands.client import CodempDisconnectCommand +from .plugin.commands.client import CodempCreateWorkspaceCommand +from .plugin.commands.client import CodempDeleteWorkspaceCommand +from .plugin.commands.client import CodempJoinWorkspaceCommand +from .plugin.commands.client import CodempLeaveWorkspaceCommand +from .plugin.commands.client import CodempInviteToWorkspaceCommand + +from .plugin.commands.workspace import CodempCreateBufferCommand +from .plugin.commands.workspace import CodempDeleteBufferCommand +from .plugin.commands.workspace import CodempJoinBufferCommand +from .plugin.commands.workspace import CodempLeaveBufferCommand + +LOG_LEVEL = logging.DEBUG +handler = logging.StreamHandler() +handler.setFormatter( + logging.Formatter( + fmt="<{thread}/{threadName}> {levelname} [{name} :: {funcName}] {message}", + style="{", + ) +) +package_logger = logging.getLogger(__package__) +package_logger.setLevel(LOG_LEVEL) +package_logger.propagate = False logger = logging.getLogger(__name__) - -# Listeners +# Initialisation and Deinitialisation ############################################################################## +def plugin_loaded(): + package_logger.addHandler(handler) + logger.debug("plugin loaded") + +def plugin_unloaded(): + logger.debug("unloading") + safe_listener_detach(TEXT_LISTENER) + package_logger.removeHandler(handler) + + +def kill_all(): + for ws in workspaces.lookup(): + session.client.leave_workspace(ws.id) + workspaces.remove(ws) + + session.stop() + + +class CodempReplaceTextCommand(sublime_plugin.TextCommand): + def run(self, edit, start, end, content, change_id): + # we modify the region to account for any change that happened in the mean time + region = self.view.transform_region_from(sublime.Region(start, end), change_id) + self.view.replace(edit, region, content) + + class EventListener(sublime_plugin.EventListener): def is_enabled(self): - return client.codemp is not None + return session.is_active() def on_exit(self): - client.disconnect() - if client.driver is not None: - client.driver.stop() + kill_all() + # client.disconnect() + # if client.driver is not None: + # client.driver.stop() def on_pre_close_window(self, window): assert client.codemp is not None @@ -113,5 +164,23 @@ class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): logger.debug(f"local buffer change! {vbuff.id}") vbuff.send_buffer_change(changes) - TEXT_LISTENER = CodempClientTextChangeListener() + + + + + +# Proxy Commands ( NOT USED, left just in case we need it again. ) +############################################################################# +# class ProxyCodempShareCommand(sublime_plugin.WindowCommand): +# # on_window_command, does not trigger when called from the command palette +# # See: https://github.com/sublimehq/sublime_text/issues/2234 +# def run(self, **kwargs): +# self.window.run_command("codemp_share", kwargs) +# +# def input(self, args): +# if 'sublime_buffer' not in args: +# return SublimeBufferPathInputHandler() +# +# def input_description(self): +# return 'Share Buffer:' diff --git a/plugin.py b/plugin.py deleted file mode 100644 index f132e5e..0000000 --- a/plugin.py +++ /dev/null @@ -1,72 +0,0 @@ -# pyright: reportIncompatibleMethodOverride=false -import sublime -import sublime_plugin -import logging - -from .src.utils import safe_listener_detach -from listeners import TEXT_LISTENER - -from client_commands import CodempConnectCommand -from client_commands import CodempDisconnectCommand -from client_commands import CodempCreateWorkspaceCommand -from client_commands import CodempDeleteWorkspaceCommand -from client_commands import CodempJoinWorkspaceCommand -from client_commands import CodempLeaveWorkspaceCommand -from client_commands import CodempInviteToWorkspaceCommand - -from workspace_commands import CodempCreateBufferCommand -from workspace_commands import CodempDeleteBufferCommand -from workspace_commands import CodempJoinBufferCommand -from workspace_commands import CodempLeaveBufferCommand - -LOG_LEVEL = logging.DEBUG -handler = logging.StreamHandler() -handler.setFormatter( - logging.Formatter( - fmt="<{thread}/{threadName}> {levelname} [{name} :: {funcName}] {message}", - style="{", - ) -) -package_logger = logging.getLogger(__package__) -package_logger.addHandler(handler) -package_logger.setLevel(LOG_LEVEL) -package_logger.propagate = False -logger = logging.getLogger(__name__) - - -# Initialisation and Deinitialisation -############################################################################## -def plugin_loaded(): - logger.debug("plugin loaded") - - -def plugin_unloaded(): - logger.debug("unloading") - safe_listener_detach(TEXT_LISTENER) - package_logger.removeHandler(handler) - # client.disconnect() - - -# Text Change Command -############################################################################# -class CodempReplaceTextCommand(sublime_plugin.TextCommand): - def run(self, edit, start, end, content, change_id): - # we modify the region to account for any change that happened in the mean time - region = self.view.transform_region_from(sublime.Region(start, end), change_id) - self.view.replace(edit, region, content) - - -# Proxy Commands ( NOT USED, left just in case we need it again. ) -############################################################################# -# class ProxyCodempShareCommand(sublime_plugin.WindowCommand): -# # on_window_command, does not trigger when called from the command palette -# # See: https://github.com/sublimehq/sublime_text/issues/2234 -# def run(self, **kwargs): -# self.window.run_command("codemp_share", kwargs) -# -# def input(self, args): -# if 'sublime_buffer' not in args: -# return SublimeBufferPathInputHandler() -# -# def input_description(self): -# return 'Share Buffer:' diff --git a/client_commands.py b/plugin/commands/client.py similarity index 90% rename from client_commands.py rename to plugin/commands/client.py index b2121cc..16db9dd 100644 --- a/client_commands.py +++ b/plugin/commands/client.py @@ -5,10 +5,12 @@ import sublime_plugin import logging import random -from .src.client import client +from ...lib import codemp +from ..core.session import session +from ..core.registry import workspaces + from input_handlers import SimpleTextInput from input_handlers import SimpleListInput -from input_handlers import ActiveWorkspacesIdList logger = logging.getLogger(__name__) @@ -17,19 +19,21 @@ logger = logging.getLogger(__name__) # Connect Command class CodempConnectCommand(sublime_plugin.WindowCommand): def is_enabled(self) -> bool: - return client.codemp is None + return True def run(self, server_host, user_name, password): # pyright: ignore[reportIncompatibleMethodOverride] - logger.info(f"Connecting to {server_host} with user {user_name}...") def _(): try: - client.connect(server_host, user_name, password) + config = codemp.get_default_config() + config.host = server_host + config.username = user_name + config.password = password + 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): @@ -58,10 +62,13 @@ class CodempConnectCommand(sublime_plugin.WindowCommand): # Disconnect Command class CodempDisconnectCommand(sublime_plugin.WindowCommand): def is_enabled(self): - return client.codemp is not None + return session.is_active() def run(self): - client.disconnect() + for ws in workspaces.lookup(): + ws.uninstall() + + session.disconnect() # Join Workspace Command @@ -108,7 +115,8 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand): # 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 + 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 @@ -176,7 +184,8 @@ class CodempDeleteWorkspaceCommand(sublime_plugin.WindowCommand): return if not client.codemp.leave_workspace(workspace_id): logger.debug("error while leaving the workspace:") - return + raise RuntimeError("error while leaving the workspace") + client.uninstall_workspace(vws) client.codemp.delete_workspace(workspace_id) diff --git a/workspace_commands.py b/plugin/commands/workspace.py similarity index 100% rename from workspace_commands.py rename to plugin/commands/workspace.py diff --git a/src/buffers.py b/plugin/core/buffers.py similarity index 100% rename from src/buffers.py rename to plugin/core/buffers.py diff --git a/src/client.py b/plugin/core/client.py similarity index 56% rename from src/client.py rename to plugin/core/client.py index 22a5a9d..d40523e 100644 --- a/src/client.py +++ b/plugin/core/client.py @@ -1,25 +1,17 @@ from __future__ import annotations from typing import Optional +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ..workspace.workspace import VirtualWorkspace import sublime import logging -import codemp -from .workspace import VirtualWorkspace -from .buffers import VirtualBuffer -from .utils import bidict +from ..utils import bidict logger = logging.getLogger(__name__) -# the client will be responsible to keep track of everything! -# it will need 3 bidirectional dictionaries and 2 normal ones -# normal: workspace_id -> VirtualWorkspaces -# normal: buffer_id -> VirtualBuffer -# bidir: VirtualBuffer <-> VirtualWorkspace -# bidir: VirtualBuffer <-> Sublime.View -# bidir: VirtualWorkspace <-> Sublime.Window - class VirtualClient: def __init__(self): @@ -34,53 +26,6 @@ class VirtualClient: self._buff2workspace: bidict[VirtualBuffer, VirtualWorkspace] = bidict() self._workspace2window: dict[VirtualWorkspace, sublime.Window] = {} - def all_workspaces( - self, window: Optional[sublime.Window] = None - ) -> list[VirtualWorkspace]: - if window is None: - return list(self._workspace2window.keys()) - else: - return [ - ws - for ws in self._workspace2window - if self._workspace2window[ws] == window - ] - - def workspace_from_view(self, view: sublime.View) -> Optional[VirtualWorkspace]: - buff = self._view2buff.get(view, None) - return self.workspace_from_buffer(buff) if buff is not None else None - - def workspace_from_buffer(self, vbuff: VirtualBuffer) -> Optional[VirtualWorkspace]: - return self._buff2workspace.get(vbuff, None) - - def workspace_from_id(self, id: str) -> Optional[VirtualWorkspace]: - return self._id2workspace.get(id) - - def all_buffers( - self, workspace: Optional[VirtualWorkspace | str] = None - ) -> list[VirtualBuffer]: - if workspace is None: - return list(self._id2buffer.values()) - elif isinstance(workspace, str): - workspace = client._id2workspace[workspace] - return self._buff2workspace.inverse.get(workspace, []) - else: - return self._buff2workspace.inverse.get(workspace, []) - - def buffer_from_view(self, view: sublime.View) -> Optional[VirtualBuffer]: - return self._view2buff.get(view) - - def buffer_from_id(self, id: str) -> Optional[VirtualBuffer]: - return self._id2buffer.get(id) - - def view_from_buffer(self, buff: VirtualBuffer) -> sublime.View: - return buff.view - - def register_buffer(self, workspace: VirtualWorkspace, buffer: VirtualBuffer): - self._buff2workspace[buffer] = workspace - self._id2buffer[buffer.id] = buffer - self._view2buff[buffer.view] = buffer - def disconnect(self): if self.codemp is None: return @@ -141,11 +86,6 @@ class VirtualClient: vws.uninstall() - def unregister_buffer(self, buffer: VirtualBuffer): - del self._buff2workspace[buffer] - del self._id2buffer[buffer.id] - del self._view2buff[buffer.view] - def workspaces_in_server(self): return self.codemp.active_workspaces() if self.codemp else [] diff --git a/plugin/core/registry.py b/plugin/core/registry.py new file mode 100644 index 0000000..60e18b4 --- /dev/null +++ b/plugin/core/registry.py @@ -0,0 +1,5 @@ +from .workspace import WorkspaceRegistry +from .buffers import BufferRegistry + +workspaces = WorkspaceRegistry() +buffers = BufferRegistry() \ No newline at end of file diff --git a/plugin/core/session.py b/plugin/core/session.py new file mode 100644 index 0000000..43532fd --- /dev/null +++ b/plugin/core/session.py @@ -0,0 +1,60 @@ +import logging +from ...lib import codemp + +logger = logging.getLogger(__name__) + +class SessionManager(): + def __init__(self) -> None: + self._running = False + self._driver = None + self._client = None + + def is_active(self): + return self._driver is not None \ + and self._running \ + and self._client is not None + + @property + def client(self): + return self._client + + def get_or_init(self) -> codemp.Driver: + if self._driver: + return self._driver + + self._driver = codemp.init() + logger.debug("registering logger callback...") + if not codemp.set_logger(lambda msg: logger.debug(msg), False): + logger.debug( + "could not register the logger... \ + If reconnecting it's ok, \ + the previous logger is still registered" + ) + self._running = True + return self._driver + + def stop(self): + if not self._driver: + return + + self.disconnect() + self._driver.stop() + self._running = False + self._driver = None + + def connect(self, config: codemp.Config) -> codemp.Client: + if not self._running: + 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 + + self._client = None + + +session = SessionManager() \ No newline at end of file diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py new file mode 100644 index 0000000..17aa051 --- /dev/null +++ b/plugin/core/workspace.py @@ -0,0 +1,221 @@ +from __future__ import annotations +from typing import Optional, Tuple +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ...main import CodempClientTextChangeListener + from ...lib import codemp + +import sublime +import shutil +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 + + if name == "": + entry = {"path": folder} + else: + entry = {"name": name, "path": folder} + + proj["folders"].append(entry) + + w.set_project_data(proj) + +def remove_project_folder(w: sublime.Window, filterstr: str): + proj: dict = self.window.project_data() # type:ignore + if proj is None: + return + + clean_proj_folders = list( + filter( + lambda f: f.get("name", "") != filterstr, + proj["folders"], + ) + ) + proj["folders"] = clean_proj_folders + w.set_project_data(proj) + +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() + + def __del__(self): + 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}'." + ) + + + 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() + + def lookup(self, w: sublime.Window | None = None) -> list[WorkspaceManager]: + if not w: + return list(self._workspaces.keys()) + ws = self._workspaces.inverse.get(w) + return ws if ws else [] + + 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): + win = sublime.active_window() + + tmpdir = tempfile.mkdtemp(prefix="codemp_") + name = f"{g.WORKSPACE_FOLDER_PREFIX}{wshandle.id()}" + add_project_folder(win, tmpdir, name) + + wm = WorkspaceManager(wshandle, win, tmpdir) + self._workspaces[wm] = win + + def remove(self, ws: WorkspaceManager | str | None): + 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) + del self._workspaces[ws] + + + + + + + + + + diff --git a/src/globals.py b/plugin/globals.py similarity index 100% rename from src/globals.py rename to plugin/globals.py diff --git a/plugin/input_handlers.py b/plugin/input_handlers.py new file mode 100644 index 0000000..5beb604 --- /dev/null +++ b/plugin/input_handlers.py @@ -0,0 +1,155 @@ +import sublime_plugin +import logging + +from typing import Tuple, Union, List + +# Input handlers +############################################################ +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:] + + def initial_text(self): + if isinstance(self.default, str): + return self.default + else: + return "" + + def name(self): + return self.argname + + def next_input(self, args): + if len(self.next_inputs) > 0: + if self.next_inputs[0][0] not in args: + if isinstance(self.next_inputs[0][1], list): + return SimpleListInput(*self.next_inputs) + else: + return SimpleTextInput(*self.next_inputs) + + +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:] + + def name(self): + return self.argname + + def list_items(self): + if isinstance(self.list, list): + return self.list + else: + return [self.list] + + def next_input(self, args): + if len(self.next_inputs) > 0: + if self.next_inputs[0][0] not in args: + if isinstance(self.next_inputs[0][1], str): + return SimpleTextInput(*self.next_inputs) + else: + return SimpleListInput(*self.next_inputs) + + +# class ActiveWorkspacesIdList(sublime_plugin.ListInputHandler): +# def __init__(self, window=None, buffer_list=False, buffer_text=False): +# self.window = window +# self.buffer_list = buffer_list +# self.buffer_text = buffer_text + +# def name(self): +# return "workspace_id" + +# def list_items(self): +# return [vws.id for vws in client.all_workspaces(self.window)] + +# def next_input(self, args): +# if self.buffer_list: +# return BufferIdList(args["workspace_id"]) +# elif self.buffer_text: +# return SimpleTextInput(("buffer_id", "new buffer")) + + +# # To allow for having a selection and choosing non existing workspaces +# # we do a little dance: We pass this list input handler to a TextInputHandler +# # when we select "Create New..." which adds his result to the list of possible +# # workspaces and pop itself off the stack to go back to the list handler. +# class WorkspaceIdList(sublime_plugin.ListInputHandler): +# def __init__(self): +# assert client.codemp is not None # the command should not be available + +# # at the moment, the client can't give us a full list of existing workspaces +# # so a textinputhandler would be more appropriate. but we keep this for the future + +# self.add_entry_text = "* add entry..." +# self.list = client.codemp.list_workspaces(True, True).wait() +# self.list.sort() +# self.list.append(self.add_entry_text) +# self.preselected = None + +# def name(self): +# return "workspace_id" + +# def placeholder(self): +# return "Workspace" + +# def list_items(self): +# if self.preselected is not None: +# return (self.list, self.preselected) +# else: +# return self.list + +# def next_input(self, args): +# if args["workspace_id"] == self.add_entry_text: +# return AddListEntry(self) + + +# class BufferIdList(sublime_plugin.ListInputHandler): +# def __init__(self, workspace_id): +# vws = client.workspace_from_id(workspace_id) +# self.add_entry_text = "* create new..." +# self.list = vws.codemp.filetree(None) +# self.list.sort() +# self.list.append(self.add_entry_text) +# self.preselected = None + +# def name(self): +# return "buffer_id" + +# def placeholder(self): +# return "Buffer Id" + +# def list_items(self): +# if self.preselected is not None: +# return (self.list, self.preselected) +# else: +# return self.list + +# def next_input(self, args): +# if args["buffer_id"] == self.add_entry_text: +# 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 + + def name(self): + return "" + + 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 next_input(self, args): + return sublime_plugin.BackInputHandler() diff --git a/src/utils.py b/plugin/utils.py similarity index 100% rename from src/utils.py rename to plugin/utils.py diff --git a/src/workspace.py b/src/workspace.py index f246c5f..e69de29 100644 --- a/src/workspace.py +++ b/src/workspace.py @@ -1,133 +0,0 @@ -from __future__ import annotations -from typing import Optional, Tuple - -from ..listeners import CodempClientTextChangeListener -import sublime -import shutil -import tempfile -import logging - -import codemp -from . import globals as g -from .buffers import VirtualBuffer -from .utils import draw_cursor_region - -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.codemp: codemp.Workspace = handle - self.window: sublime.Window = window - self.curctl: codemp.CursorController = self.codemp.cursor() - - self.id: str = self.codemp.id() - - self.codemp.fetch_buffers() - self.codemp.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.codemp.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 all_buffers(self) -> list[VirtualBuffer]: - return list(self._id2buff.values()) - - def buff_by_id(self, id: str) -> Optional[VirtualBuffer]: - return self._id2buff.get(id) - - 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 uninstall_buffer(self, vbuff: VirtualBuffer): - del self._id2buff[vbuff.id] - self.codemp.detach(vbuff.id) - vbuff.uninstall() - - 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) From da7c2bb60ac3dac79b74355c9b46a43173738a44 Mon Sep 17 00:00:00 2001 From: cschen Date: Wed, 18 Sep 2024 16:57:26 +0200 Subject: [PATCH 02/13] wip: continuing refactor, added buffers Former-commit-id: 96208df478a2b9534668331d6a25118778822f9b --- plugin/commands/client.py | 18 +++-- plugin/core/buffers.py | 141 +++++++++++++++++++++----------------- plugin/core/registry.py | 2 +- plugin/core/session.py | 16 ++--- plugin/core/workspace.py | 133 ++++++----------------------------- plugin/globals.py | 8 +-- 6 files changed, 117 insertions(+), 201 deletions(-) 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 From 2077773cd942e895b8b940077493ddb1126573f0 Mon Sep 17 00:00:00 2001 From: cschen Date: Wed, 18 Sep 2024 23:45:52 +0200 Subject: [PATCH 03/13] fix: clarify message that the plugin is not functional at the moment Former-commit-id: 689ae167668fbfff637db22c73821911f4c661e8 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52b0eac..7ce047b 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ as well as a remote virtual workspace for you and your team. # Codemp-Sublime This is the reference [sublime](https://sublimetext.com) plugin for `codemp` maintained by [hexedtech](https://hexed.technology) -> [!IMPORTANT] -> The plugin is in active development. Expect frequent changes. +> [!WARNING] +> The plugin is currently not working, pending merging with the sublime text package repository. +> The codemp wheel dependency must be registered before the current version works. # Installation > [!IMPORTANT] From 7cf6ac7cbcf33e08a9f43aaf0c3296ba8bc642b9 Mon Sep 17 00:00:00 2001 From: cschen Date: Fri, 20 Sep 2024 17:24:53 +0200 Subject: [PATCH 04/13] fix: the fuck, where do these CIs come from??? --- .github/workflows/CI.yml | 120 --------------------------------------- 1 file changed, 120 deletions(-) delete mode 100644 .github/workflows/CI.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index 8268eaf..0000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,120 +0,0 @@ -# This file is autogenerated by maturin v1.1.0 -# To update, run -# -# maturin generate-ci github -# -name: CI - -on: - push: - branches: - - main - - master - tags: - - '*' - pull_request: - workflow_dispatch: - -permissions: - contents: read - -jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - manylinux: auto - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - windows: - runs-on: windows-latest - strategy: - matrix: - target: [x64, x86] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - architecture: ${{ matrix.target }} - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - macos: - runs-on: macos-latest - strategy: - matrix: - target: [x86_64, aarch64] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - sdist: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --out dist - - name: Upload sdist - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - - release: - name: Release - runs-on: ubuntu-latest - if: "startsWith(github.ref, 'refs/tags/')" - needs: [linux, windows, macos, sdist] - steps: - - uses: actions/download-artifact@v3 - with: - name: wheels - - name: Publish to PyPI - uses: PyO3/maturin-action@v1 - env: - MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - with: - command: upload - args: --skip-existing * From e0b56ccc2933dfa3e38fe80f37db493ddd5dd64f Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 28 Sep 2024 19:55:20 +0200 Subject: [PATCH 05/13] fix: switched to the proper config chore: switched to bundled codemp library in sublime fix: fixed circular dependency contd: small step towards rewrite. --- main.py | 6 +-- plugin/commands/client.py | 28 +++++------- plugin/core/buffers.py | 30 ++++++------ plugin/core/client.py | 96 --------------------------------------- plugin/core/registry.py | 5 -- plugin/core/session.py | 3 +- plugin/core/workspace.py | 19 ++++---- src/workspace.py | 0 8 files changed, 39 insertions(+), 148 deletions(-) delete mode 100644 plugin/core/client.py delete mode 100644 plugin/core/registry.py delete mode 100644 src/workspace.py diff --git a/main.py b/main.py index fd8432a..016d28b 100644 --- a/main.py +++ b/main.py @@ -3,11 +3,11 @@ import sublime import sublime_plugin import logging -from lib import codemp +import codemp from .plugin.utils import safe_listener_detach from .plugin.core.session import session -from .plugin.core.registry import workspaces -from .plugin.core.registry import buffers +from .plugin.core.workspace import workspaces +from .plugin.core.buffers import buffers from .plugin.commands.client import CodempConnectCommand from .plugin.commands.client import CodempDisconnectCommand diff --git a/plugin/commands/client.py b/plugin/commands/client.py index ca63cda..52335b7 100644 --- a/plugin/commands/client.py +++ b/plugin/commands/client.py @@ -3,10 +3,10 @@ import sublime_plugin import logging import random -from ...lib import codemp +import codemp from ..core.session import session -from ..core.registry import workspaces -from ..core.registry import buffers +from ..core.workspace import workspaces +from ..core.buffers import buffers from input_handlers import SimpleTextInput from input_handlers import SimpleListInput @@ -14,14 +14,13 @@ 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.get_default_config() - config.host = server_host - config.username = user_name - config.password = password + config = codemp.Config( + username = user_name, + password = password, + host = server_host) session.connect(config) except Exception as e: sublime.error_message( @@ -72,15 +71,14 @@ class CodempDisconnectCommand(sublime_plugin.WindowCommand): # Join Workspace Command class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand): def is_enabled(self) -> bool: - return client.codemp is not None + return session.is_active() def run(self, workspace_id): # pyright: ignore[reportIncompatibleMethodOverride] - assert client.codemp is not None if workspace_id is None: return logger.info(f"Joining workspace: '{workspace_id}'...") - promise = client.codemp.join_workspace(workspace_id) + promise = session.client.join_workspace(workspace_id) active_window = sublime.active_window() def _(): @@ -92,19 +90,15 @@ class CodempJoinWorkspaceCommand(sublime_plugin.WindowCommand): ) sublime.error_message(f"Could not join workspace '{workspace_id}'") return - client.install_workspace(workspace, active_window) - + workspaces.add(workspace) sublime.set_timeout_async(_) - # the else shouldn't really happen, and if it does, it should already be instantiated. - # ignore. def input_description(self): return "Join:" def input(self, args): - assert client.codemp is not None if "workspace_id" not in args: - list = client.codemp.list_workspaces(True, True) + list = session.client.list_workspaces(True, True) return SimpleListInput( ("workspace_id", list.wait()), ) diff --git a/plugin/core/buffers.py b/plugin/core/buffers.py index 7df05b2..1da5d8b 100644 --- a/plugin/core/buffers.py +++ b/plugin/core/buffers.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from .workspace import WorkspaceManager - from ...lib import codemp + import codemp import sublime import os @@ -16,10 +16,10 @@ from ..utils import bidict logger = logging.getLogger(__name__) -def bind_buffer_manager(buff: BufferManager): - def text_callback(bufctl: codemp.BufferController): +def bind_callback(v: sublime.View): + def _callback(bufctl: codemp.BufferController): def _(): - change_id = buff.view.change_id() + change_id = v.change_id() while change := bufctl.try_recv().wait(): logger.debug("received remote buffer change!") if change is None: @@ -32,13 +32,13 @@ def bind_buffer_manager(buff: BufferManager): # 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 + if v.id() == g.ACTIVE_CODEMP_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. - buff.view.run_command( + v.run_command( "codemp_replace_text", { "start": change.start, @@ -48,7 +48,7 @@ def bind_buffer_manager(buff: BufferManager): }, # pyright: ignore ) sublime.set_timeout(_) - return text_callback + return _callback class BufferManager(): def __init__(self, handle: codemp.BufferController, v: sublime.View, filename: str): @@ -56,6 +56,7 @@ class BufferManager(): self.view: sublime.View = v self.id = self.handle.path() self.filename = filename + self.handle.callback(bind_callback(self.view)) def __del__(self): logger.debug(f"dropping buffer {self.id}") @@ -80,7 +81,6 @@ class BufferManager(): def sync(self, text_listener): promise = self.handle.content() - def _(): content = promise.wait() safe_listener_detach(text_listener) @@ -92,13 +92,13 @@ class BufferRegistry(): def __init__(self): self._buffers: bidict[BufferManager, WorkspaceManager] = bidict() - def lookup(self, ws: WorkspaceManager | None = None) -> list[BufferManager]: + 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 [] - def lookupId(self, bid: str) -> BufferManager | None: + def lookupId(self, bid: str) -> Optional[BufferManager]: return next((bf for bf in self._buffers if bf.id == bid), None) def add(self, bhandle: codemp.BufferController, wsm: WorkspaceManager): @@ -115,17 +115,17 @@ class BufferRegistry(): view.set_status(g.SUBLIME_STATUS_ID, "[Codemp]") bfm = BufferManager(bhandle, view, tmpfile) + self._buffers[bfm] = wsm - def remove(self, bf: BufferManager | str | None): + def remove(self, bf: Optional[BufferManager | str]): if isinstance(bf, str): bf = self.lookupId(bf) - if not bf: return del self._buffers[bf] bf.view.close() - +buffers = BufferRegistry() diff --git a/plugin/core/client.py b/plugin/core/client.py deleted file mode 100644 index d40523e..0000000 --- a/plugin/core/client.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import annotations -from typing import Optional -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from ..workspace.workspace import VirtualWorkspace - -import sublime -import logging - -from ..utils import bidict - -logger = logging.getLogger(__name__) - - -class VirtualClient: - def __init__(self): - self.codemp: Optional[codemp.Client] = None - self.driver: Optional[codemp.Driver] = None - - # bookkeeping corner - self._id2buffer: dict[str, VirtualBuffer] = {} - self._id2workspace: dict[str, VirtualWorkspace] = {} - - self._view2buff: dict[sublime.View, VirtualBuffer] = {} - self._buff2workspace: bidict[VirtualBuffer, VirtualWorkspace] = bidict() - self._workspace2window: dict[VirtualWorkspace, sublime.Window] = {} - - def disconnect(self): - if self.codemp is None: - return - logger.info("disconnecting from the current client") - # for each workspace tell it to clean up after itself. - for vws in self.all_workspaces(): - self.uninstall_workspace(vws) - self.codemp.leave_workspace(vws.id) - - self._id2workspace.clear() - self._id2buffer.clear() - self._buff2workspace.clear() - self._view2buff.clear() - self._workspace2window.clear() - - if self.driver is not None: - self.driver.stop() - self.driver = None - self.codemp = None - - def connect(self, host: str, user: str, password: str): - if self.codemp is not None: - logger.info("Disconnecting from previous client.") - return self.disconnect() - - if self.driver is None: - self.driver = codemp.init() - logger.debug("registering logger callback...") - if not codemp.set_logger(lambda msg: logger.debug(msg), False): - logger.debug( - "could not register the logger... If reconnecting it's ok, the previous logger is still registered" - ) - - config = codemp.get_default_config() - config.username = user - config.host = host - config.password = password - - self.codemp = codemp.connect(config).wait() - id = self.codemp.user_id() - logger.debug(f"Connected to '{host}' as user {user} (id: {id})") - - def install_workspace(self, workspace: codemp.Workspace, window: sublime.Window): - vws = VirtualWorkspace(workspace, window) - self._workspace2window[vws] = window - self._id2workspace[vws.id] = vws - - def uninstall_workspace(self, vws: VirtualWorkspace): - # we aim at dropping all references to the workspace - # as well as all the buffers associated with it. - # if we did a good job the dunder del method will kick - # and continue with the cleanup. - logger.info(f"Uninstalling workspace '{vws.id}'...") - del self._workspace2window[vws] - del self._id2workspace[vws.id] - for vbuff in self.all_buffers(vws): - self.unregister_buffer(vbuff) - - vws.uninstall() - - def workspaces_in_server(self): - return self.codemp.active_workspaces() if self.codemp else [] - - def user_id(self): - return self.codemp.user_id() if self.codemp else None - - -client = VirtualClient() diff --git a/plugin/core/registry.py b/plugin/core/registry.py deleted file mode 100644 index 443c62d..0000000 --- a/plugin/core/registry.py +++ /dev/null @@ -1,5 +0,0 @@ -from .workspace import WorkspaceRegistry -from .buffers import BufferRegistry - -workspaces = WorkspaceRegistry() -buffers = BufferRegistry() diff --git a/plugin/core/session.py b/plugin/core/session.py index 5c6d290..82f5f33 100644 --- a/plugin/core/session.py +++ b/plugin/core/session.py @@ -1,5 +1,5 @@ import logging -from ...lib import codemp +import codemp logger = logging.getLogger(__name__) @@ -54,5 +54,4 @@ class SessionManager(): def drop_client(self): self._client = None - session = SessionManager() \ No newline at end of file diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index a3edc78..7c907b1 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -3,7 +3,7 @@ from typing import Optional, Tuple from typing import TYPE_CHECKING if TYPE_CHECKING: from ...main import CodempClientTextChangeListener - from ...lib import codemp + import codemp import sublime import shutil @@ -13,7 +13,7 @@ import logging from .. import globals as g from ..utils import draw_cursor_region from ..utils import bidict -from ..core.registry import buffers +from .buffers import buffers logger = logging.getLogger(__name__) @@ -64,11 +64,11 @@ class WorkspaceManager(): self.curctl: codemp.CursorController = self.handle.cursor() self.rootdir: str = rootdir self.id: str = self.handle.id() + self.curctl.callback(cursor_callback) def __del__(self): logger.debug(f"dropping workspace {self.id}") self.curctl.clear_callback() - self.curctl.stop() for buff in self.handle.buffer_list(): if not self.handle.detach(buff): @@ -76,9 +76,8 @@ class WorkspaceManager(): f"could not detach from '{buff}' for workspace '{self.id}'." ) - bfm = buffers.lookupId(buff) - if bfm: - buffers.remove(bfm) + for bfm in buffers.lookup(self): + 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 @@ -89,13 +88,13 @@ class WorkspaceRegistry(): def __init__(self) -> None: self._workspaces: bidict[WorkspaceManager, sublime.Window] = bidict() - def lookup(self, w: sublime.Window | None = None) -> list[WorkspaceManager]: + def lookup(self, w: Optional[sublime.Window] = None) -> list[WorkspaceManager]: if not w: return list(self._workspaces.keys()) ws = self._workspaces.inverse.get(w) return ws if ws else [] - def lookupId(self, wid: str) -> WorkspaceManager | None: + def lookupId(self, wid: str) -> Optional[WorkspaceManager]: return next((ws for ws in self._workspaces if ws.id == wid), None) def add(self, wshandle: codemp.Workspace) -> WorkspaceManager: @@ -109,7 +108,7 @@ class WorkspaceRegistry(): self._workspaces[wm] = win return wm - def remove(self, ws: WorkspaceManager | str | None): + def remove(self, ws: Optional[WorkspaceManager | str]): if isinstance(ws, str): ws = self.lookupId(ws) @@ -120,7 +119,7 @@ class WorkspaceRegistry(): del self._workspaces[ws] - +workspaces = WorkspaceRegistry() diff --git a/src/workspace.py b/src/workspace.py deleted file mode 100644 index e69de29..0000000 From 75f4e185a8cd9ed5621dd880f96ab428b55e3f78 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 2 Nov 2024 18:26:37 +0100 Subject: [PATCH 06/13] 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 From c222aec0133c16a1d658099b76160c3743af9c34 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 2 Nov 2024 18:30:13 +0100 Subject: [PATCH 07/13] chore: missed the file with the text change listener class. It was moved in its own file because this function is crucial and needs some extra attention. --- plugin/text_listener.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 plugin/text_listener.py diff --git a/plugin/text_listener.py b/plugin/text_listener.py new file mode 100644 index 0000000..bd57685 --- /dev/null +++ b/plugin/text_listener.py @@ -0,0 +1,32 @@ +import sublime_plugin +import logging + +from .core.buffers import buffers +from . import globals as g + +logger = logging.getLogger(__name__) + +class CodempClientTextChangeListener(sublime_plugin.TextChangeListener): + @classmethod + def is_applicable(cls, _): # 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 + + bid = str(s.get(g.CODEMP_BUFFER_ID)) + try: + vbuff = buffers.lookupId(bid) + logger.debug(f"local buffer change! {vbuff.id}") + vbuff.send_change(changes) + except KeyError: + logger.error(f"could not find registered buffer with id {bid}") + pass + +TEXT_LISTENER = CodempClientTextChangeListener() \ No newline at end of file From 93f3e38c75a11c6b2310643cd84315eefd133f63 Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 3 Nov 2024 17:57:31 +0100 Subject: [PATCH 08/13] feat(WIP): added quick panel browsing for server and workspace! --- Codemp.sublime-commands | 36 +++-- main.py | 40 ++++++ plugin/quickpanel/qp_globals.py | 24 ++++ plugin/quickpanel/qpbrowser.py | 224 ++++++++++++++++++++++++++++++++ 4 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 plugin/quickpanel/qp_globals.py create mode 100644 plugin/quickpanel/qpbrowser.py diff --git a/Codemp.sublime-commands b/Codemp.sublime-commands index f7037bf..b467cf9 100644 --- a/Codemp.sublime-commands +++ b/Codemp.sublime-commands @@ -19,24 +19,38 @@ "file": "${packages}/CodempClient/README.md" } }, + { + "caption": "Codemp: Browse Server", + "command": "codemp_browse_server", + "args": {} + }, + { + "caption": "Codemp: Browse Workspace", + "command": "codemp_browse_workspace", + "args": { + + } + }, { // # on_window_command, does not trigger when called from the command palette // # See: https://github.com/sublimehq/sublime_text/issues/2234 "caption": "Codemp: Connect", "command": "codemp_connect", "args": { - "server_host": "http://code.mp:50053", + "server_host": "code.mp", + "user_name" : "cschen@codemp.dev", + "password" : "***REMOVED***" } }, { "caption": "Codemp: Disconnect Client", "command": "codemp_disconnect", - "arg": {} + "args": {} }, { "caption": "Codemp: Join Workspace", "command": "codemp_join_workspace", - "arg": { + "args": { // 'workspace_id': 'asd' // 'buffer_id': 'test' }, @@ -44,14 +58,14 @@ { "caption": "Codemp: Leave Workspace", "command": "codemp_leave_workspace", - "arg": { + "args": { // "id": 'lmaaaao' } }, { "caption": "Codemp: Invite To Workspace", "command": "codemp_invite_to_workspace", - "arg": { + "args": { // "id": 'lmaaaao' // "user": 'lupo' } @@ -59,21 +73,21 @@ { "caption": "Codemp: Create Workspace", "command": "codemp_create_workspace", - "arg": { + "args": { // "id": 'lmaaaao' } }, { "caption": "Codemp: Delete Workspace", "command": "codemp_delete_workspace", - "arg": { + "args": { // "id": 'lmaaaao' } }, { "caption": "Codemp: Join Buffer", "command": "codemp_join_buffer", - "arg": { + "args": { // 'workspace_id': 'asd' // 'buffer_id': 'test' }, @@ -81,7 +95,7 @@ { "caption": "Codemp: Leave Buffer", "command": "codemp_leave_buffer", - "arg": { + "args": { // 'workspace_id': 'asd' // 'buffer_id': 'test' } @@ -89,7 +103,7 @@ { "caption": "Codemp: Create Buffer", "command": "codemp_create_buffer", - "arg": { + "args": { // 'workspace_id': 'asd' // 'buffer_id': 'test' } @@ -97,7 +111,7 @@ { "caption": "Codemp: Delete Buffer", "command": "codemp_delete_buffer", - "arg": { + "args": { // 'workspace_id': 'asd' // 'buffer_id': 'test' } diff --git a/main.py b/main.py index 1924f10..892a173 100644 --- a/main.py +++ b/main.py @@ -5,10 +5,14 @@ import logging import codemp from .plugin.utils import safe_listener_detach +from .plugin.utils import safe_listener_attach from .plugin.core.session import session from .plugin.core.workspace import workspaces from .plugin.core.buffers import buffers +from .plugin.text_listener import TEXT_LISTENER +from .plugin import globals as g +# We import these just to showcase the commands available. from .plugin.commands.client import CodempConnectCommand from .plugin.commands.client import CodempDisconnectCommand from .plugin.commands.client import CodempCreateWorkspaceCommand @@ -22,6 +26,10 @@ from .plugin.commands.workspace import CodempDeleteBufferCommand from .plugin.commands.workspace import CodempJoinBufferCommand from .plugin.commands.workspace import CodempLeaveBufferCommand +from .plugin.quickpanel.qpbrowser import QPServerBrowser +from .plugin.quickpanel.qpbrowser import QPWorkspaceBrowser + + LOG_LEVEL = logging.DEBUG handler = logging.StreamHandler() handler.setFormatter( @@ -54,6 +62,38 @@ def kill_all(): session.stop() +def objects_from_view(view): + assert view.settings().get(g.CODEMP_VIEW_TAG, False) + buffid = str(view.settings().get(g.CODEMP_BUFFER_ID)) + + try: vbuff = buffers.lookupId(buffid) + except KeyError: + logger.error("we couldn't find the matching buffer or workspace!") + raise ValueError + + vws = buffers.lookupParent(vbuff) + win = workspaces.lookupParent(vws) + + return win, vws, vbuff + +class CodempBrowseWorkspaceCommand(sublime_plugin.WindowCommand): + def is_enabled(self) -> bool: + return session.is_active() + + def run(self, workspace_id): + wks = workspaces.lookupId(workspace_id) + buffers = wks.handle.fetch_buffers() + QPWorkspaceBrowser(self.window, workspace_id, buffers.wait()).run() + + +class CodempBrowseServerCommand(sublime_plugin.WindowCommand): + def is_enabled(self) -> bool: + return session.is_active() + + def run(self): + wks = session.get_workspaces() + QPServerBrowser(self.window, session.host, wks).run() + class CodempReplaceTextCommand(sublime_plugin.TextCommand): def run(self, edit, start, end, content, change_id): diff --git a/plugin/quickpanel/qp_globals.py b/plugin/quickpanel/qp_globals.py new file mode 100644 index 0000000..f4785a8 --- /dev/null +++ b/plugin/quickpanel/qp_globals.py @@ -0,0 +1,24 @@ +import sublime + +QP_COLOR_NONE = sublime.KIND_ID_AMBIGUOUS +QP_COLOR_REDISH = sublime.KIND_ID_COLOR_REDISH +QP_COLOR_ORANGISH = sublime.KIND_ID_COLOR_ORANGISH +QP_COLOR_YELLOWISH = sublime.KIND_ID_COLOR_YELLOWISH +QP_COLOR_GREENISH = sublime.KIND_ID_COLOR_GREENISH +QP_COLOR_CYANISH = sublime.KIND_ID_COLOR_CYANISH +QP_COLOR_BLUISH = sublime.KIND_ID_COLOR_BLUISH +QP_COLOR_PURPLISH = sublime.KIND_ID_COLOR_PURPLISH +QP_COLOR_PINKISH = sublime.KIND_ID_COLOR_PINKISH +QP_COLOR_DARK = sublime.KIND_ID_COLOR_DARK +QP_COLOR_LIGHT = sublime.KIND_ID_COLOR_LIGHT + +QP_YES = "✓" +QP_NO = "✗" +QP_ADD = "+" +QP_FORWARD = "❯" +QP_BACK = "❮" +QP_DETAILS = "⋯" +QP_RENAME = "*" +QP_CHMOD = "7" +QP_DOWNLOAD = "⏬" +QP_EDIT = "a" diff --git a/plugin/quickpanel/qpbrowser.py b/plugin/quickpanel/qpbrowser.py new file mode 100644 index 0000000..859062e --- /dev/null +++ b/plugin/quickpanel/qpbrowser.py @@ -0,0 +1,224 @@ +import sublime +import logging + +from . import qp_globals as qpg + +from ..core.workspace import workspaces +from ..core.buffers import buffers + +logger = logging.getLogger(__name__) + +def qpi(text, details="", color=qpg.QP_COLOR_NONE, letter="", name="", hint="", prefix=""): + return sublime.QuickPanelItem(text, details, annotation=hint, kind=(color, letter, name)) + +def show_qp(window, choices, on_done, placeholder=''): + def _(): + flags = sublime.KEEP_OPEN_ON_FOCUS_LOST + window.show_quick_panel(choices, on_done, flags, placeholder=placeholder) + sublime.set_timeout(_, 10) + + +class QPServerBrowser(): + def __init__(self, window, host, raw_input_items): + self.window = window + self.host = host + self.raw_input_items = raw_input_items + + def make_entry(self, wsid): + return qpi(wsid, letter="w", color=qpg.QP_COLOR_BLUISH, hint="Workspace", prefix=" ") + + def qp_placeholder(self): + return f"Browsing workspaces on host: {self.host}" + + def run(self): + self.current_wid_selection = None + self.entries = [] + for item in self.raw_input_items: + self.entries.append(self.make_entry(item)) + + self.entries.insert(0, qpi("Server Actions", + color=qpg.QP_COLOR_CYANISH, + letter=qpg.QP_DETAILS, + hint="Submenu", + prefix=" • ")) + + show_qp(self.window, self.entries, self.server_actions, self.qp_placeholder()) + + def server_actions(self, index): + if index == -1: + return + elif index == 0: + self.edit_server() + return + + wid = self.entries[index].trigger + self.current_wid_selection = wid + # self.select_workspace() + def _(): + self.window.run_command( + "codemp_join_workspace", + { + "workspace_id": self.current_wid_selection, + "sync": True + }) + + ws = workspaces.lookupId(wid) + buffers = ws.handle.fetch_buffers() + QPWorkspaceBrowser(self.window, wid, buffers.wait()).run() + sublime.set_timeout(_) + logger.debug("exiting the server_broswer.") + + def select_workspace(self): + assert self.current_wid_selection + actions = [ + qpi("Join", details=self.current_wid_selection, color=qpg.QP_COLOR_BLUISH, letter=qpg.QP_FORWARD), + # qpi("Join and open all", + # details="opens all buffer in the workspace", + # color=qpg.QP_COLOR_PINKISH, letter=qpg.QP_DETAILS), + qpi("Back", color=qpg.QP_COLOR_BLUISH, letter=qpg.QP_BACK) + ] + show_qp(self.window, actions, self.select_workspace_actions, self.qp_placeholder()) + + def select_workspace_actions(self, index): + if index == -1: + return + elif index == 0: + self.window.run_command( + "codemp_join_workspace", + {"workspace_id": self.current_wid_selection}) + elif index == 1: + self.run() + + + def edit_server(self): + actions = [ + qpi("Back", color=qpg.QP_COLOR_CYANISH, letter=qpg.QP_BACK), + qpi("New Workspace", color=qpg.QP_COLOR_GREENISH, letter=qpg.QP_ADD), + qpi("Delete Workspace", color=qpg.QP_COLOR_REDISH, letter=qpg.QP_NO) + ] + show_qp(self.window, actions, self.edit_server_actions, self.qp_placeholder()) + + def edit_server_actions(self, index): + if index == -1: + return + + if index == 0: + self.run() + + if index == 1: + def create_workspace(name): + self.window.run_command( + "codemp_create_workspace", + {"workspace_id": name}) + self.window.show_input_panel("New Workspace Name", "", create_workspace, None, self.edit_server) + + if index == 2: + def delete_workspace(index): + if index == -1 or index == 0: + self.edit_server() + # we must be careful here. here with index 1 we are selecting the correct + # workspace, because the index zero in the entries is the workspace action submenu. + # which is occupied by the back action. + # if we add extra non workspace entries, then we must shift the index accordingly. + # Do this differently? + selected = self.entries[index] + self.window.run_command( + "codemp_delete_workspace", + {"workspace_id": selected.trigger}) + + + show_qp(self.window, self.entries, delete_workspace, self.qp_placeholder()) + + +class QPWorkspaceBrowser(): + def __init__(self, window, workspace_id, raw_input_items): + self.window = window + self.workspace_id = workspace_id + self.raw_input_items = raw_input_items + + def qp_placeholder(self): + return f"Browsing buffers in {self.workspace_id}" + + def make_entry(self, item): + return qpi(item, letter="b", color=qpg.QP_COLOR_BLUISH, hint="Buffer", prefix=" ") + + def run(self): + self.entries = [] + for buffer in self.raw_input_items: + self.entries.append(self.make_entry(buffer)) + + self.entries.insert(0, qpi("Workspace Actions", + color=qpg.QP_COLOR_CYANISH, + letter=qpg.QP_DETAILS, + hint="Submenu", + prefix=" • ")) + + show_qp(self.window, self.entries, self.workspace_actions, self.qp_placeholder()) + + def workspace_actions(self, index): + if index == -1: + return + elif index == 0: + self.edit_workspace() + return + + bid = self.entries[index].trigger + + self.window.run_command( + "codemp_join_buffer", + { + "workspace_id": self.workspace_id, + "buffer_id": bid + }) + + + def edit_workspace(self): + actions = [ + qpi("Back", color=qpg.QP_COLOR_CYANISH, letter=qpg.QP_BACK), + qpi("Leave Workspace", color=qpg.QP_COLOR_ORANGISH, letter=qpg.QP_BACK), + qpi("Invite User", color=qpg.QP_COLOR_PINKISH, letter=qpg.QP_FORWARD), + qpi("Create Buffer", color=qpg.QP_COLOR_GREENISH, letter=qpg.QP_ADD), + qpi("Delete Buffer", color=qpg.QP_COLOR_REDISH, letter=qpg.QP_NO), + qpi("Rename Buffer", color=qpg.QP_COLOR_ORANGISH, letter=qpg.QP_RENAME), + ] + show_qp(self.window, actions, self.edit_workspace_actions, self.qp_placeholder()) + + def edit_workspace_actions(self, index): + if index == -1 or index == 0: + self.edit_workspace() + elif index == 1: + self.window.run_command( + "codemp_leave_workspace", + {"workspace_id": self.workspace_id}) + self.window.run_command( + "codemp_browse_server", {}) + elif index == 2: + self.window.run_command( + "codemp_invite_to_workspace", + {"workspace_id": self.workspace_id}) + elif index == 3: + def create_buffer(name): + self.window.run_command( + "codemp_create_workspace", + { + "workspace_id": self.workspace_id, + "buffer_id": name + }) + self.window.show_input_panel("New Workspace Name", "", create_buffer, None, self.edit_workspace) + elif index == 4: + def delete_buffer(index): + if index == -1 or index == 0: + self.edit_workspace() + + # same warning as the server browser. Check your indexed 3 times + selected = self.entries[index] + self.window.run_command( + "codemp_delete_buffer", + { + "workspace_id": self.workspace_id, + "buffer_id": selected.trigger + }) + show_qp(self.window, self.entries, delete_buffer, self.qp_placeholder()) + elif index == 5: + sublime.message_dialog("renaming is not yet implemented.") + self.edit_workspace() \ No newline at end of file From 713fe3a30c452a8e3b42f9bc9300524436356a2b Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 3 Nov 2024 17:58:17 +0100 Subject: [PATCH 09/13] chore: disable create on missing behaviour for now --- plugin/commands/workspace.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugin/commands/workspace.py b/plugin/commands/workspace.py index 289b07a..ab6a59f 100644 --- a/plugin/commands/workspace.py +++ b/plugin/commands/workspace.py @@ -53,17 +53,17 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand): except KeyError: pass - # 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?", - ): - sublime.run_command("codemp_create_buffer", { - "workspace_id": workspace_id, - "buffer_id": buffer_id - }) + # # 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?", + # ): + # 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}...") From d03b7753cc0c9352894b5b6ba76d572fc8c3d05b Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 3 Nov 2024 17:58:45 +0100 Subject: [PATCH 10/13] fix: wrongly calling command to create workspace instead of buffer. --- plugin/quickpanel/qpbrowser.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugin/quickpanel/qpbrowser.py b/plugin/quickpanel/qpbrowser.py index 859062e..253ad98 100644 --- a/plugin/quickpanel/qpbrowser.py +++ b/plugin/quickpanel/qpbrowser.py @@ -57,10 +57,7 @@ class QPServerBrowser(): def _(): self.window.run_command( "codemp_join_workspace", - { - "workspace_id": self.current_wid_selection, - "sync": True - }) + {"workspace_id": self.current_wid_selection}) ws = workspaces.lookupId(wid) buffers = ws.handle.fetch_buffers() @@ -199,12 +196,12 @@ class QPWorkspaceBrowser(): elif index == 3: def create_buffer(name): self.window.run_command( - "codemp_create_workspace", + "codemp_create_buffer", { "workspace_id": self.workspace_id, "buffer_id": name }) - self.window.show_input_panel("New Workspace Name", "", create_buffer, None, self.edit_workspace) + self.window.show_input_panel("New Buffer Name", "", create_buffer, None, self.edit_workspace) elif index == 4: def delete_buffer(index): if index == -1 or index == 0: From a01fb8f29197b3d61d9c2f92bf1bd621826ba2e9 Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 3 Nov 2024 17:59:17 +0100 Subject: [PATCH 11/13] fix/tweaks: minor changes and fixes --- main.py | 26 +++++++++++--------------- plugin/commands/client.py | 2 +- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/main.py b/main.py index 892a173..d35d828 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import logging import codemp from .plugin.utils import safe_listener_detach from .plugin.utils import safe_listener_attach +from .plugin.utils import some from .plugin.core.session import session from .plugin.core.workspace import workspaces from .plugin.core.buffers import buffers @@ -66,10 +67,7 @@ def objects_from_view(view): assert view.settings().get(g.CODEMP_VIEW_TAG, False) buffid = str(view.settings().get(g.CODEMP_BUFFER_ID)) - try: vbuff = buffers.lookupId(buffid) - except KeyError: - logger.error("we couldn't find the matching buffer or workspace!") - raise ValueError + vbuff = buffers.lookupId(buffid) vws = buffers.lookupParent(vbuff) win = workspaces.lookupParent(vws) @@ -92,7 +90,7 @@ class CodempBrowseServerCommand(sublime_plugin.WindowCommand): def run(self): wks = session.get_workspaces() - QPServerBrowser(self.window, session.host, wks).run() + QPServerBrowser(self.window, session.config.host, wks).run() class CodempReplaceTextCommand(sublime_plugin.TextCommand): @@ -113,8 +111,6 @@ class EventListener(sublime_plugin.EventListener): # client.driver.stop() def on_pre_close_window(self, window): - assert session.client is not None - for vws in workspaces.lookup(window): sublime.run_command("codemp_leave_workspace", { "workspace_id": vws.id @@ -145,9 +141,9 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): try: _, vws, vbuff = objects_from_view(self.view) - except ValueError: + except KeyError: logger.error(f"Could not find buffers associated with the view {self.view}.\ - Removig the tag to disable event listener. Reattach.") + Removing 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 @@ -156,24 +152,24 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}") def on_activated(self): - global TEXT_LISTENER logger.debug(f"'{self.view}' view activated!") safe_listener_attach(TEXT_LISTENER, self.view.buffer()) # pyright: ignore def on_deactivated(self): - global TEXT_LISTENER logger.debug(f"'{self.view}' view deactivated!") safe_listener_detach(TEXT_LISTENER) # pyright: ignore def on_pre_close(self): if self.view == sublime.active_window().active_view(): logger.debug("closing active view") - global TEXT_LISTENER safe_listener_detach(TEXT_LISTENER) # pyright: ignore try: - _, vws, vbuff = objects_from_view(self.view) - buffers.remove(vbuff) - except ValueError: + bid = str(self.view.settings().get(g.CODEMP_BUFFER_ID)) + vws = buffers.lookupParent(bid) + some(self.view.window()).run_command( + "codemp_leave_buffer", + {"workspace_id": vws.id, "buffer_id": bid}) + except KeyError: return def on_text_command(self, command_name, args): diff --git a/plugin/commands/client.py b/plugin/commands/client.py index ad5f553..be560e4 100644 --- a/plugin/commands/client.py +++ b/plugin/commands/client.py @@ -167,7 +167,7 @@ class CodempDeleteWorkspaceCommand(sublime_plugin.WindowCommand): 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) + return SimpleListInput(("workspace_id", workspaces)) def run(self, workspace_id: str): # pyright: ignore[reportIncompatibleMethodOverride] try: From b378173fb4993f10f7ef8835da63b9a7168c52de Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 19 Nov 2024 19:46:44 +0100 Subject: [PATCH 12/13] feat: Add sync command. --- Codemp.sublime-commands | 5 +++++ main.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/Codemp.sublime-commands b/Codemp.sublime-commands index b467cf9..ac813da 100644 --- a/Codemp.sublime-commands +++ b/Codemp.sublime-commands @@ -116,4 +116,9 @@ // 'buffer_id': 'test' } }, + { + "caption": "Codemp: Sync", + "command": "codemp_sync_buffer", + "args": {} + } ] \ No newline at end of file diff --git a/main.py b/main.py index d35d828..86ad0a9 100644 --- a/main.py +++ b/main.py @@ -100,6 +100,12 @@ class CodempReplaceTextCommand(sublime_plugin.TextCommand): self.view.replace(edit, region, content) +class CodempSyncBuffer(sublime_plugin.TextCommand): + def run(self, edit): + buff = buffers.lookupId(self.view.settings().get(g.CODEMP_BUFFER_ID)) + buff.sync(TEXT_LISTENER) + + class EventListener(sublime_plugin.EventListener): def is_enabled(self): return session.is_active() From 63da225cbeb5109e64f95781702f7061f44e1285 Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 19 Nov 2024 19:50:31 +0100 Subject: [PATCH 13/13] disables using `change_id()` as it was introducing complications. moved view population outside of buffer adding, but it is still broken. --- main.py | 8 ++-- plugin/commands/workspace.py | 13 +++--- plugin/core/buffers.py | 79 +++++++++++++++++++++--------------- plugin/core/workspace.py | 2 +- plugin/text_listener.py | 1 + plugin/utils.py | 4 +- 6 files changed, 64 insertions(+), 43 deletions(-) diff --git a/main.py b/main.py index 86ad0a9..be9fb52 100644 --- a/main.py +++ b/main.py @@ -94,9 +94,11 @@ class CodempBrowseServerCommand(sublime_plugin.WindowCommand): class CodempReplaceTextCommand(sublime_plugin.TextCommand): - def run(self, edit, start, end, content, change_id): + def run(self, edit, start, end, content, change_id = None): # we modify the region to account for any change that happened in the mean time - region = self.view.transform_region_from(sublime.Region(start, end), change_id) + region = sublime.Region(start, end) + if change_id: + region = self.view.transform_region_from(sublime.Region(start, end), change_id) self.view.replace(edit, region, content) @@ -155,7 +157,7 @@ class CodempClientViewEventListener(sublime_plugin.ViewEventListener): return vws.send_cursor(vbuff.id, start, end) - logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}") + # logger.debug(f"selection modified! {vws.id}, {vbuff.id} - {start}, {end}") def on_activated(self): logger.debug(f"'{self.view}' view activated!") diff --git a/plugin/commands/workspace.py b/plugin/commands/workspace.py index ab6a59f..1e45924 100644 --- a/plugin/commands/workspace.py +++ b/plugin/commands/workspace.py @@ -7,7 +7,7 @@ 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 ..utils import safe_listener_attach, safe_listener_detach, populate_view from ..input_handlers import SimpleListInput, SimpleTextInput logger = logging.getLogger(__name__) @@ -67,11 +67,11 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand): # now we can defer the attaching process logger.debug(f"attempting to attach to {buffer_id}...") - promise = vws.handle.attach_buffer(buffer_id) + ctl_promise = vws.handle.attach_buffer(buffer_id) def _(): try: - buff_ctl = promise.wait() + buff_ctl = ctl_promise.wait() logger.debug("attach successfull!") except Exception as e: logging.error(f"error when attaching to buffer '{id}':\n\n {e}") @@ -79,8 +79,11 @@ class CodempJoinBufferCommand(sublime_plugin.WindowCommand): return safe_listener_detach(TEXT_LISTENER) + content_promise = buff_ctl.content() vbuff = buffers.add(buff_ctl, vws) + content = content_promise.wait() + populate_view(vbuff.view, content) 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()) @@ -115,8 +118,8 @@ class CodempLeaveBufferCommand(sublime_plugin.WindowCommand): 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}'") + sublime.error_message(f"You are not attached to the buffer '{buffer_id}'") + logging.warning(f"You are not attached to the buffer '{buffer_id}'") return if not vws.handle.get_buffer(buffer_id): diff --git a/plugin/core/buffers.py b/plugin/core/buffers.py index 4995293..697bc8e 100644 --- a/plugin/core/buffers.py +++ b/plugin/core/buffers.py @@ -7,10 +7,12 @@ if TYPE_CHECKING: import sublime import os import logging +import threading from codemp import TextChange from .. import globals as g from ..utils import populate_view +from ..utils import get_contents from ..utils import safe_listener_attach from ..utils import safe_listener_detach from ..utils import bidict @@ -18,44 +20,54 @@ from ..utils import bidict logger = logging.getLogger(__name__) def bind_callback(v: sublime.View): + # we need this lock to prevent multiple instance of try_recv() to spin up + # which would cause out of order insertion of changes. + multi_tryrecv_lock = threading.Lock() + def _callback(bufctl: codemp.BufferController): def _(): - change_id = v.change_id() - while buffup := bufctl.try_recv().wait(): - logger.debug("received remote buffer change!") - if buffup is None: - break + try: + # change_id = v.change_id() + change_id = None + while buffup := bufctl.try_recv().wait(): + logger.debug("received remote buffer change!") + if buffup is None: + break - if buffup.change.is_empty(): - logger.debug("change is empty. skipping.") - continue + 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 == sublime.active_window().active_view(): - v.settings()[g.CODEMP_IGNORE_NEXT_TEXT_CHANGE] = True + # 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 == 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. - - # 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. - try: change = buffup.change v.run_command( "codemp_replace_text", { - "start": change.start, - "end": change.end, + "start": change.start_idx, + "end": change.end_idx, "content": change.content, "change_id": change_id, }, # pyright: ignore ) - except Exception as e: - raise e - bufctl.ack(buffup.version) - sublime.set_timeout(_) + bufctl.ack(buffup.version) + except Exception as e: + raise e + finally: + logger.debug("releasing lock") + multi_tryrecv_lock.release() + + if multi_tryrecv_lock.acquire(blocking=False): + logger.debug("acquiring lock") + sublime.set_timeout(_) return _callback class BufferManager(): @@ -78,11 +90,11 @@ class BufferManager(): # 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 - ) - ) + # 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.handle.send(TextChange(start=region.begin(), end=region.end(), content=change.str)) @@ -91,9 +103,14 @@ class BufferManager(): promise = self.handle.content() def _(): content = promise.wait() + current_contents = get_contents(self.view) + 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(_) class BufferRegistry(): @@ -120,8 +137,7 @@ class BufferRegistry(): bid = bhandle.path() # 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) @@ -129,7 +145,6 @@ class BufferRegistry(): 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) diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index cc213b6..5370b84 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -51,7 +51,7 @@ def cursor_callback(ctl: codemp.CursorController): def _(): while event := ctl.try_recv().wait(): if event is None: break - + try: bfm = buffers.lookupId(event.sel.buffer) except KeyError: continue diff --git a/plugin/text_listener.py b/plugin/text_listener.py index bd57685..9443ebd 100644 --- a/plugin/text_listener.py +++ b/plugin/text_listener.py @@ -1,3 +1,4 @@ +import sublime import sublime_plugin import logging diff --git a/plugin/utils.py b/plugin/utils.py index 16e9a72..4f8ae27 100644 --- a/plugin/utils.py +++ b/plugin/utils.py @@ -88,7 +88,7 @@ def populate_view(view, content): "start": 0, "end": view.size(), "content": content, - "change_id": view.change_id(), + "change_id": None, }, ) @@ -105,7 +105,7 @@ def draw_cursor_region(view, start, end, user): reg_flags = sublime.RegionFlags.DRAW_EMPTY user_hash = hash(user) - + view.add_regions( f"{g.SUBLIME_REGIONS_PREFIX}-{user_hash}", [reg],