wip: huge refactor with smarter and simpler struture

Former-commit-id: fbd0bca8094642dd8d2cf6c3f154af3b10dff95b
This commit is contained in:
cschen 2024-09-17 22:20:00 +02:00
parent 74705d94c0
commit 539a4e4e5e
15 changed files with 545 additions and 450 deletions

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
/target
test*
/lib
lib
# Byte-compiled / optimized / DLL files
__pycache__/

View file

@ -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()

View file

@ -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:'

View file

@ -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:'

View file

@ -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)

View file

@ -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
View file

@ -0,0 +1,5 @@
from .workspace import WorkspaceRegistry
from .buffers import BufferRegistry
workspaces = WorkspaceRegistry()
buffers = BufferRegistry()

60
plugin/core/session.py Normal file
View 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
View 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
View 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()

View file

@ -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)