mirror of
https://github.com/hexedtech/codemp-sublime.git
synced 2024-11-22 06:44:48 +01:00
wip: huge refactor with smarter and simpler struture
Former-commit-id: fbd0bca8094642dd8d2cf6c3f154af3b10dff95b
This commit is contained in:
parent
74705d94c0
commit
539a4e4e5e
15 changed files with 545 additions and 450 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
|||
/target
|
||||
test*
|
||||
/lib
|
||||
lib
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
|
|
@ -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()
|
|
@ -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:'
|
72
plugin.py
72
plugin.py
|
@ -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:'
|
|
@ -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)
|
|
@ -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 []
|
||||
|
5
plugin/core/registry.py
Normal file
5
plugin/core/registry.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from .workspace import WorkspaceRegistry
|
||||
from .buffers import BufferRegistry
|
||||
|
||||
workspaces = WorkspaceRegistry()
|
||||
buffers = BufferRegistry()
|
60
plugin/core/session.py
Normal file
60
plugin/core/session.py
Normal file
|
@ -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()
|
221
plugin/core/workspace.py
Normal file
221
plugin/core/workspace.py
Normal file
|
@ -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]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
155
plugin/input_handlers.py
Normal file
155
plugin/input_handlers.py
Normal file
|
@ -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()
|
133
src/workspace.py
133
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)
|
Loading…
Reference in a new issue