From 539a4e4e5e67e28a01204e6d5df5da38de75a361 Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 17 Sep 2024 22:20:00 +0200 Subject: [PATCH] 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)